change (member moderation) : fix all existing tests
This commit is contained in:
parent
e405bf80a6
commit
0b6c5964a1
12 changed files with 64 additions and 726 deletions
|
|
@ -35,6 +35,7 @@ import io.element.android.features.messages.impl.timeline.protection.aTimelinePr
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
|
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
|
||||||
import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider
|
import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider
|
||||||
import io.element.android.features.roomcall.api.aStandByCallState
|
import io.element.android.features.roomcall.api.aStandByCallState
|
||||||
|
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
||||||
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
@ -1182,6 +1183,9 @@ class MessagesPresenterTest {
|
||||||
textEditorState = aTextEditorStateMarkdown(initialText = "", initialFocus = false)
|
textEditorState = aTextEditorStateMarkdown(initialText = "", initialFocus = false)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
roomMemberModerationPresenter: Presenter<RoomMemberModerationState> = Presenter {
|
||||||
|
aRoomMemberModerationState()
|
||||||
|
},
|
||||||
encryptionService: FakeEncryptionService = FakeEncryptionService(),
|
encryptionService: FakeEncryptionService = FakeEncryptionService(),
|
||||||
actionListEventSink: (ActionListEvents) -> Unit = {},
|
actionListEventSink: (ActionListEvents) -> Unit = {},
|
||||||
): MessagesPresenter {
|
): MessagesPresenter {
|
||||||
|
|
@ -1199,6 +1203,7 @@ class MessagesPresenterTest {
|
||||||
linkPresenter = { aLinkState() },
|
linkPresenter = { aLinkState() },
|
||||||
pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() },
|
pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() },
|
||||||
roomCallStatePresenter = { aStandByCallState() },
|
roomCallStatePresenter = { aStandByCallState() },
|
||||||
|
roomMemberModerationPresenter = roomMemberModerationPresenter,
|
||||||
syncService = FakeSyncService(),
|
syncService = FakeSyncService(),
|
||||||
snackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher = SnackbarDispatcher(),
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,9 @@ import io.element.android.features.messages.impl.timeline.components.receipt.bot
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
@ -310,40 +313,42 @@ class MessagesViewTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Config(qualifiers = "h1024dp")
|
@Config(qualifiers = "h1024dp")
|
||||||
fun `clicking on the avatar of the sender of an Event invoke expected callback`() {
|
fun `clicking on the avatar of the sender of an Event emits the expected event`() {
|
||||||
val eventsRecorder = EventsRecorder<MessagesEvents>(expectEvents = false)
|
val eventsRecorder = EventsRecorder<MessagesEvents>()
|
||||||
val state = aMessagesState(
|
val state = aMessagesState(
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
)
|
)
|
||||||
val timelineItem = state.timelineState.timelineItems.first()
|
val timelineEvent = state.timelineState.timelineItems.filterIsInstance<TimelineItem.Event>().first()
|
||||||
ensureCalledOnceWithParam(
|
rule.setMessagesView(state = state)
|
||||||
param = (timelineItem as TimelineItem.Event).senderId
|
rule.onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick()
|
||||||
) { callback ->
|
eventsRecorder.assertSingle(
|
||||||
rule.setMessagesView(
|
MessagesEvents.OnUserClicked(
|
||||||
state = state,
|
MatrixUser(
|
||||||
onUserDataClick = callback,
|
userId = timelineEvent.senderId,
|
||||||
|
displayName = timelineEvent.senderProfile.getDisplayName(),
|
||||||
|
avatarUrl = timelineEvent.senderProfile.getAvatarUrl()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
rule.onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick()
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Config(qualifiers = "h1024dp")
|
@Config(qualifiers = "h1024dp")
|
||||||
fun `clicking on the display name of the sender of an Event invoke expected callback`() {
|
fun `clicking on the display name of the sender of an Event emits expected event`() {
|
||||||
val eventsRecorder = EventsRecorder<MessagesEvents>(expectEvents = false)
|
val eventsRecorder = EventsRecorder<MessagesEvents>()
|
||||||
val state = aMessagesState(
|
val state = aMessagesState(eventSink = eventsRecorder)
|
||||||
eventSink = eventsRecorder
|
val timelineEvent = state.timelineState.timelineItems.filterIsInstance<TimelineItem.Event>().first()
|
||||||
)
|
rule.setMessagesView(state = state)
|
||||||
val timelineItem = state.timelineState.timelineItems.first()
|
rule.onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick()
|
||||||
ensureCalledOnceWithParam(
|
eventsRecorder.assertSingle(
|
||||||
param = (timelineItem as TimelineItem.Event).senderId
|
MessagesEvents.OnUserClicked(
|
||||||
) { callback ->
|
MatrixUser(
|
||||||
rule.setMessagesView(
|
userId = timelineEvent.senderId,
|
||||||
state = state,
|
displayName = timelineEvent.senderProfile.getDisplayName(),
|
||||||
onUserDataClick = callback,
|
avatarUrl = timelineEvent.senderProfile.getAvatarUrl()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
rule.onNodeWithTag(TestTags.timelineItemSenderName.value, useUnmergedTree = true).performClick()
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemList
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
|
@ -99,7 +100,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setPinne
|
||||||
state: PinnedMessagesListState,
|
state: PinnedMessagesListState,
|
||||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||||
onEventClick: (event: TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
onEventClick: (event: TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(),
|
onUserDataClick: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onLinkClick: (Link) -> Unit = EnsureNeverCalledWithParam(),
|
onLinkClick: (Link) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onLinkLongClick: (Link) -> Unit = EnsureNeverCalledWithParam(),
|
onLinkLongClick: (Link) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.core.UniqueId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||||
|
|
@ -175,7 +176,7 @@ class TimelineViewTest {
|
||||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimelineView(
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimelineView(
|
||||||
state: TimelineState,
|
state: TimelineState,
|
||||||
timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(),
|
timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(),
|
||||||
onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(),
|
onUserDataClick: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onLinkClick: (Link) -> Unit = EnsureNeverCalledWithParam(),
|
onLinkClick: (Link) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onMessageClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
onMessageClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onMessageLongClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
onMessageLongClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import io.element.android.services.analytics.api.AnalyticsService
|
||||||
class RoomMemberListNode @AssistedInject constructor(
|
class RoomMemberListNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
presenterFactory: RoomMemberListPresenter.Factory,
|
private val presenter: RoomMemberListPresenter,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val roomMemberModerationRenderer: RoomMemberModerationRenderer,
|
private val roomMemberModerationRenderer: RoomMemberModerationRenderer,
|
||||||
) : Node(buildContext, plugins = plugins), RoomMemberListNavigator {
|
) : Node(buildContext, plugins = plugins), RoomMemberListNavigator {
|
||||||
|
|
@ -39,7 +39,6 @@ class RoomMemberListNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callbacks = plugins<Callback>()
|
private val callbacks = plugins<Callback>()
|
||||||
private val presenter = presenterFactory.create(this)
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,6 @@ import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedFactory
|
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.roommembermoderation.api.ModerationAction
|
import io.element.android.features.roommembermoderation.api.ModerationAction
|
||||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
|
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
|
||||||
|
|
@ -53,12 +51,7 @@ class RoomMemberListPresenter @AssistedInject constructor(
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
private val roomMembersModerationPresenter: Presenter<RoomMemberModerationState>,
|
private val roomMembersModerationPresenter: Presenter<RoomMemberModerationState>,
|
||||||
private val encryptionService: EncryptionService,
|
private val encryptionService: EncryptionService,
|
||||||
@Assisted private val navigator: RoomMemberListNavigator,
|
|
||||||
) : Presenter<RoomMemberListState> {
|
) : Presenter<RoomMemberListState> {
|
||||||
@AssistedFactory
|
|
||||||
interface Factory {
|
|
||||||
fun create(navigator: RoomMemberListNavigator): RoomMemberListPresenter
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): RoomMemberListState {
|
override fun present(): RoomMemberListState {
|
||||||
|
|
@ -168,10 +161,8 @@ class RoomMemberListPresenter @AssistedInject constructor(
|
||||||
is RoomMemberListEvents.RoomMemberSelected ->
|
is RoomMemberListEvents.RoomMemberSelected ->
|
||||||
if (event.roomMember.membership == RoomMembershipState.BAN) {
|
if (event.roomMember.membership == RoomMembershipState.BAN) {
|
||||||
roomModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(ModerationAction.UnbanUser, event.roomMember.toMatrixUser()))
|
roomModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(ModerationAction.UnbanUser, event.roomMember.toMatrixUser()))
|
||||||
} else if (!isDm.value && (roomModerationState.canBan || roomModerationState.canKick)) {
|
|
||||||
roomModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.roomMember.toMatrixUser()))
|
|
||||||
} else {
|
} else {
|
||||||
navigator.openRoomMemberDetails(event.roomMember.userId)
|
roomModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.roomMember.toMatrixUser()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,8 @@ import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents
|
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
||||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.features.roomdetails.impl.members.moderation.aRoomMembersModerationState
|
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -24,7 +23,6 @@ import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
|
@ -42,12 +40,12 @@ class RoomMemberListPresenterTest {
|
||||||
fun `member loading is done automatically on start, but is async`() = runTest {
|
fun `member loading is done automatically on start, but is async`() = runTest {
|
||||||
val room = FakeJoinedRoom(
|
val room = FakeJoinedRoom(
|
||||||
baseRoom = FakeBaseRoom(
|
baseRoom = FakeBaseRoom(
|
||||||
updateMembersResult = { Result.success(Unit) },
|
updateMembersResult = { Result.success(Unit) },
|
||||||
canInviteResult = { Result.success(true) }
|
canInviteResult = { Result.success(true) }
|
||||||
).apply {
|
).apply {
|
||||||
// Needed to avoid discarding the loaded members as a partial and invalid result
|
// Needed to avoid discarding the loaded members as a partial and invalid result
|
||||||
givenRoomInfo(aRoomInfo(joinedMembersCount = 2))
|
givenRoomInfo(aRoomInfo(joinedMembersCount = 2))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
val presenter = createPresenter(joinedRoom = room)
|
val presenter = createPresenter(joinedRoom = room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -97,9 +95,9 @@ class RoomMemberListPresenterTest {
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
joinedRoom = FakeJoinedRoom(
|
joinedRoom = FakeJoinedRoom(
|
||||||
baseRoom = FakeBaseRoom(
|
baseRoom = FakeBaseRoom(
|
||||||
updateMembersResult = { Result.success(Unit) },
|
updateMembersResult = { Result.success(Unit) },
|
||||||
canInviteResult = { Result.success(true) }
|
canInviteResult = { Result.success(true) }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -204,12 +202,12 @@ class RoomMemberListPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - RoomMemberSelected by default opens the room member details through the navigator`() = runTest {
|
fun `present - RoomMemberSelected will open the moderation options when target user is not banned`() = runTest {
|
||||||
val navigator = FakeRoomMemberListNavigator()
|
val roomMemberModerationPresenter= Presenter {
|
||||||
val roomMembersModerationStateLambda = { aRoomMembersModerationState(canDisplayModerationActions = false) }
|
aRoomMemberModerationState(canBan = true, canKick = true)
|
||||||
|
}
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
roomMembersModerationStateLambda = roomMembersModerationStateLambda,
|
roomMemberModerationPresenter = roomMemberModerationPresenter,
|
||||||
navigator = navigator,
|
|
||||||
joinedRoom = FakeJoinedRoom(
|
joinedRoom = FakeJoinedRoom(
|
||||||
baseRoom = FakeBaseRoom(
|
baseRoom = FakeBaseRoom(
|
||||||
updateMembersResult = { Result.success(Unit) },
|
updateMembersResult = { Result.success(Unit) },
|
||||||
|
|
@ -222,36 +220,6 @@ class RoomMemberListPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
awaitItem().eventSink(RoomMemberListEvents.RoomMemberSelected(aVictor()))
|
awaitItem().eventSink(RoomMemberListEvents.RoomMemberSelected(aVictor()))
|
||||||
assertThat(navigator.openRoomMemberDetailsCallCount).isEqualTo(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - RoomMemberSelected will open the moderation options if the current user can use them`() = runTest {
|
|
||||||
val navigator = FakeRoomMemberListNavigator()
|
|
||||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
|
||||||
val roomMembersModerationStateLambda = {
|
|
||||||
aRoomMembersModerationState(
|
|
||||||
canDisplayModerationActions = true,
|
|
||||||
eventSink = eventsRecorder,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val presenter = createPresenter(
|
|
||||||
roomMembersModerationStateLambda = roomMembersModerationStateLambda,
|
|
||||||
navigator = navigator,
|
|
||||||
joinedRoom = FakeJoinedRoom(
|
|
||||||
baseRoom = FakeBaseRoom(
|
|
||||||
updateMembersResult = { Result.success(Unit) },
|
|
||||||
canInviteResult = { Result.success(true) }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
skipItems(1)
|
|
||||||
awaitItem().eventSink(RoomMemberListEvents.RoomMemberSelected(aVictor()))
|
|
||||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.SelectRoomMember(aVictor()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -277,19 +245,19 @@ private fun TestScope.createDataSource(
|
||||||
private fun TestScope.createPresenter(
|
private fun TestScope.createPresenter(
|
||||||
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
|
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
|
||||||
joinedRoom: JoinedRoom = FakeJoinedRoom(
|
joinedRoom: JoinedRoom = FakeJoinedRoom(
|
||||||
baseRoom = FakeBaseRoom(
|
baseRoom = FakeBaseRoom(
|
||||||
updateMembersResult = { Result.success(Unit) }
|
updateMembersResult = { Result.success(Unit) }
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
roomMemberListDataSource: RoomMemberListDataSource = createDataSource(coroutineDispatchers = coroutineDispatchers),
|
roomMemberListDataSource: RoomMemberListDataSource = createDataSource(coroutineDispatchers = coroutineDispatchers),
|
||||||
roomMembersModerationStateLambda: () -> RoomMembersModerationState = { aRoomMembersModerationState() },
|
|
||||||
encryptedService: FakeEncryptionService = FakeEncryptionService(),
|
encryptedService: FakeEncryptionService = FakeEncryptionService(),
|
||||||
navigator: RoomMemberListNavigator = object : RoomMemberListNavigator {}
|
roomMemberModerationPresenter: Presenter<RoomMemberModerationState> = Presenter {
|
||||||
|
aRoomMemberModerationState()
|
||||||
|
},
|
||||||
) = RoomMemberListPresenter(
|
) = RoomMemberListPresenter(
|
||||||
room = joinedRoom,
|
room = joinedRoom,
|
||||||
roomMemberListDataSource = roomMemberListDataSource,
|
roomMemberListDataSource = roomMemberListDataSource,
|
||||||
coroutineDispatchers = coroutineDispatchers,
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
roomMembersModerationPresenter = { roomMembersModerationStateLambda() },
|
roomMembersModerationPresenter = roomMemberModerationPresenter,
|
||||||
encryptionService = encryptedService,
|
encryptionService = encryptedService,
|
||||||
navigator = navigator,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,351 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl.members.moderation
|
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionMode
|
|
||||||
import app.cash.molecule.moleculeFlow
|
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import im.vector.app.features.analytics.plan.RoomModeration
|
|
||||||
import io.element.android.features.roomdetails.impl.aJoinedRoom
|
|
||||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
|
||||||
import io.element.android.features.roomdetails.impl.members.aVictor
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembersState
|
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
|
||||||
import io.element.android.libraries.matrix.test.A_REASON
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
|
||||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
|
||||||
import io.element.android.tests.testutils.lambda.value
|
|
||||||
import io.element.android.tests.testutils.test
|
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.coroutines.test.TestScope
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class RoomMembersModerationPresenterTest {
|
|
||||||
@Test
|
|
||||||
fun `canDisplayModerationActions - when room is DM is false`() = runTest {
|
|
||||||
val room = aJoinedRoom(
|
|
||||||
isPublic = true,
|
|
||||||
activeMemberCount = 2,
|
|
||||||
canKickResult = { Result.success(true) },
|
|
||||||
canBanResult = { Result.success(true) },
|
|
||||||
userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
|
|
||||||
).apply {
|
|
||||||
givenRoomInfo(aRoomInfo(isDirect = true, activeMembersCount = 2))
|
|
||||||
}
|
|
||||||
val presenter = createRoomMembersModerationPresenter(joinedRoom = room)
|
|
||||||
presenter.test {
|
|
||||||
assertThat(awaitItem().canDisplayModerationActions).isFalse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `canDisplayModerationActions - when user can kick other users, FF is enabled and room is not a DM returns true`() = runTest {
|
|
||||||
val room = aJoinedRoom(
|
|
||||||
activeMemberCount = 10,
|
|
||||||
canKickResult = { Result.success(true) },
|
|
||||||
canBanResult = { Result.success(true) },
|
|
||||||
userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
|
|
||||||
)
|
|
||||||
val presenter = createRoomMembersModerationPresenter(joinedRoom = room)
|
|
||||||
presenter.test {
|
|
||||||
skipItems(1)
|
|
||||||
assertThat(awaitItem().canDisplayModerationActions).isTrue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `canDisplayModerationActions - when user can ban other users, FF is enabled and room is not a DM returns true`() = runTest {
|
|
||||||
val room = aJoinedRoom(
|
|
||||||
activeMemberCount = 10,
|
|
||||||
canKickResult = { Result.success(true) },
|
|
||||||
canBanResult = { Result.success(true) },
|
|
||||||
userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
|
|
||||||
)
|
|
||||||
val presenter = createRoomMembersModerationPresenter(joinedRoom = room)
|
|
||||||
presenter.test {
|
|
||||||
skipItems(1)
|
|
||||||
assertThat(awaitItem().canDisplayModerationActions).isTrue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - SelectRoomMember when the current user has permissions displays member actions`() = runTest {
|
|
||||||
val room = aJoinedRoom(
|
|
||||||
canKickResult = { Result.success(true) },
|
|
||||||
canBanResult = { Result.success(true) },
|
|
||||||
userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
|
|
||||||
)
|
|
||||||
val selectedMember = aVictor()
|
|
||||||
val presenter = createRoomMembersModerationPresenter(joinedRoom = room)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
skipItems(1)
|
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
|
||||||
with(awaitItem()) {
|
|
||||||
assertThat(this.selectedRoomMember).isNotNull()
|
|
||||||
assertThat(this.selectedRoomMember?.userId).isEqualTo(selectedMember.userId)
|
|
||||||
assertThat(actions).containsExactly(
|
|
||||||
ModerationAction.DisplayProfile(selectedMember.userId),
|
|
||||||
ModerationAction.KickUser(selectedMember.userId),
|
|
||||||
ModerationAction.BanUser(selectedMember.userId)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - SelectRoomMember displays only view profile if selected member has same power level as the current user`() = runTest {
|
|
||||||
val room = aJoinedRoom(
|
|
||||||
sessionId = A_USER_ID,
|
|
||||||
canKickResult = { Result.success(true) },
|
|
||||||
canBanResult = { Result.success(true) },
|
|
||||||
userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
|
|
||||||
)
|
|
||||||
val selectedMember = aRoomMember(A_USER_ID_2, powerLevel = 100L)
|
|
||||||
val presenter = createRoomMembersModerationPresenter(joinedRoom = room)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
skipItems(1)
|
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
|
||||||
with(awaitItem()) {
|
|
||||||
assertThat(this.selectedRoomMember).isNotNull()
|
|
||||||
assertThat(this.selectedRoomMember?.userId).isEqualTo(selectedMember.userId)
|
|
||||||
assertThat(actions).containsExactly(
|
|
||||||
ModerationAction.DisplayProfile(selectedMember.userId),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - SelectRoomMember displays an unban confirmation dialog when the member is banned`() = runTest {
|
|
||||||
val selectedMember = aRoomMember(A_USER_ID_2, membership = RoomMembershipState.BAN)
|
|
||||||
val room = aJoinedRoom(
|
|
||||||
canKickResult = { Result.success(true) },
|
|
||||||
canBanResult = { Result.success(true) },
|
|
||||||
userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
|
|
||||||
)
|
|
||||||
val presenter = createRoomMembersModerationPresenter(joinedRoom = room)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
skipItems(1)
|
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
|
||||||
with(awaitItem()) {
|
|
||||||
assertThat(selectedRoomMember).isNull()
|
|
||||||
assertThat(unbanUserAsyncAction).isEqualTo(ConfirmingRoomMemberAction(selectedMember))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - Kick requires confirmation and then kicks the user`() = runTest {
|
|
||||||
val analyticsService = FakeAnalyticsService()
|
|
||||||
val kickUserResult = lambdaRecorder<UserId, String?, Result<Unit>> { _, _ -> Result.success(Unit) }
|
|
||||||
val room = aJoinedRoom(
|
|
||||||
canKickResult = { Result.success(true) },
|
|
||||||
canBanResult = { Result.success(true) },
|
|
||||||
userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
|
|
||||||
kickUserResult = kickUserResult,
|
|
||||||
)
|
|
||||||
val selectedMember = aVictor()
|
|
||||||
val presenter = createRoomMembersModerationPresenter(joinedRoom = room, analyticsService = analyticsService)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
skipItems(1)
|
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.KickUser)
|
|
||||||
val confirmingState = awaitItem()
|
|
||||||
assertThat(confirmingState.kickUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
|
||||||
// Confirm
|
|
||||||
confirmingState.eventSink(RoomMembersModerationEvents.DoKickUser(reason = A_REASON))
|
|
||||||
skipItems(1)
|
|
||||||
val loadingState = awaitItem()
|
|
||||||
assertThat(loadingState.actions).isEmpty()
|
|
||||||
assertThat(loadingState.kickUserAsyncAction).isEqualTo(AsyncAction.Loading)
|
|
||||||
with(awaitItem()) {
|
|
||||||
assertThat(kickUserAsyncAction).isEqualTo(AsyncAction.Success(Unit))
|
|
||||||
assertThat(selectedRoomMember).isNull()
|
|
||||||
}
|
|
||||||
assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.KickMember))
|
|
||||||
kickUserResult.assertions().isCalledOnce().with(
|
|
||||||
value(selectedMember.userId),
|
|
||||||
value(A_REASON),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - BanUser requires confirmation and then bans the user`() = runTest {
|
|
||||||
val analyticsService = FakeAnalyticsService()
|
|
||||||
val banUserResult = lambdaRecorder<UserId, String?, Result<Unit>> { _, _ -> Result.success(Unit) }
|
|
||||||
val room = aJoinedRoom(
|
|
||||||
canKickResult = { Result.success(true) },
|
|
||||||
canBanResult = { Result.success(true) },
|
|
||||||
userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
|
|
||||||
banUserResult = banUserResult,
|
|
||||||
)
|
|
||||||
val selectedMember = aVictor()
|
|
||||||
val presenter = createRoomMembersModerationPresenter(joinedRoom = room, analyticsService = analyticsService)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
skipItems(1)
|
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.BanUser)
|
|
||||||
val confirmingState = awaitItem()
|
|
||||||
assertThat(confirmingState.banUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
|
||||||
// Confirm
|
|
||||||
confirmingState.eventSink(RoomMembersModerationEvents.DoBanUser(reason = A_REASON))
|
|
||||||
skipItems(1)
|
|
||||||
val loadingItem = awaitItem()
|
|
||||||
assertThat(loadingItem.actions).isEmpty()
|
|
||||||
assertThat(loadingItem.selectedRoomMember).isNull()
|
|
||||||
assertThat(loadingItem.banUserAsyncAction).isEqualTo(AsyncAction.Loading)
|
|
||||||
with(awaitItem()) {
|
|
||||||
assertThat(banUserAsyncAction).isEqualTo(AsyncAction.Success(Unit))
|
|
||||||
assertThat(selectedRoomMember).isNull()
|
|
||||||
}
|
|
||||||
assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.BanMember))
|
|
||||||
banUserResult.assertions().isCalledOnce().with(
|
|
||||||
value(selectedMember.userId),
|
|
||||||
value(A_REASON),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - UnbanUser requires confirmation and then unbans the user`() = runTest {
|
|
||||||
val analyticsService = FakeAnalyticsService()
|
|
||||||
val selectedMember = aRoomMember(A_USER_ID_2, membership = RoomMembershipState.BAN)
|
|
||||||
val room = aJoinedRoom(
|
|
||||||
canKickResult = { Result.success(true) },
|
|
||||||
canBanResult = { Result.success(true) },
|
|
||||||
userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
|
|
||||||
unBanUserResult = { _, _ -> Result.success(Unit) },
|
|
||||||
).apply {
|
|
||||||
givenRoomMembersState(RoomMembersState.Ready(persistentListOf(selectedMember)))
|
|
||||||
}
|
|
||||||
val presenter = createRoomMembersModerationPresenter(joinedRoom = room, analyticsService = analyticsService)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
skipItems(1)
|
|
||||||
// Displays unban confirmation dialog
|
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
|
|
||||||
val confirmingState = awaitItem()
|
|
||||||
assertThat(confirmingState.selectedRoomMember).isNull()
|
|
||||||
assertThat(confirmingState.actions).isEmpty()
|
|
||||||
assertThat(confirmingState.unbanUserAsyncAction).isEqualTo(ConfirmingRoomMemberAction(selectedMember))
|
|
||||||
// Confirms unban
|
|
||||||
confirmingState.eventSink(RoomMembersModerationEvents.UnbanUser(selectedMember.userId))
|
|
||||||
assertThat(awaitItem().unbanUserAsyncAction).isEqualTo(AsyncAction.Loading)
|
|
||||||
with(awaitItem()) {
|
|
||||||
assertThat(unbanUserAsyncAction).isEqualTo(AsyncAction.Success(Unit))
|
|
||||||
assertThat(selectedRoomMember).isNull()
|
|
||||||
}
|
|
||||||
assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.UnbanMember))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - Reset removes the selected user and actions`() = runTest {
|
|
||||||
val room = aJoinedRoom(
|
|
||||||
canKickResult = { Result.success(true) },
|
|
||||||
canBanResult = { Result.success(true) },
|
|
||||||
userRoleResult = { Result.success(RoomMember.Role.USER) },
|
|
||||||
)
|
|
||||||
val presenter = createRoomMembersModerationPresenter(joinedRoom = room)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
skipItems(1)
|
|
||||||
// Select a user
|
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor()))
|
|
||||||
// Reset state
|
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.Reset)
|
|
||||||
val finalItem = awaitItem()
|
|
||||||
assertThat(finalItem.selectedRoomMember).isNull()
|
|
||||||
assertThat(finalItem.actions).isEmpty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - Reset resets any async actions`() = runTest {
|
|
||||||
val room = aJoinedRoom(
|
|
||||||
canKickResult = { Result.success(true) },
|
|
||||||
canBanResult = { Result.success(true) },
|
|
||||||
kickUserResult = { _, _ -> Result.failure(Throwable("Eek")) },
|
|
||||||
banUserResult = { _, _ -> Result.failure(Throwable("Eek")) },
|
|
||||||
unBanUserResult = { _, _ -> Result.failure(Throwable("Eek")) },
|
|
||||||
userRoleResult = { Result.success(RoomMember.Role.USER) },
|
|
||||||
)
|
|
||||||
val presenter = createRoomMembersModerationPresenter(joinedRoom = room)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialItem = awaitItem()
|
|
||||||
// Kick user and fail
|
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor()))
|
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.DoKickUser(reason = ""))
|
|
||||||
skipItems(1)
|
|
||||||
assertThat(awaitItem().kickUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
|
||||||
assertThat(awaitItem().kickUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java)
|
|
||||||
// Reset it
|
|
||||||
initialItem.eventSink(RoomMembersModerationEvents.Reset)
|
|
||||||
assertThat(awaitItem().kickUserAsyncAction).isEqualTo(AsyncAction.Uninitialized)
|
|
||||||
|
|
||||||
// Ban user and fail
|
|
||||||
initialItem.eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor()))
|
|
||||||
awaitItem().eventSink(RoomMembersModerationEvents.DoBanUser(reason = ""))
|
|
||||||
skipItems(1)
|
|
||||||
assertThat(awaitItem().banUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
|
||||||
assertThat(awaitItem().banUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java)
|
|
||||||
// Reset it
|
|
||||||
initialItem.eventSink(RoomMembersModerationEvents.Reset)
|
|
||||||
assertThat(awaitItem().banUserAsyncAction).isEqualTo(AsyncAction.Uninitialized)
|
|
||||||
|
|
||||||
// Unban user and fail
|
|
||||||
initialItem.eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor().copy(membership = RoomMembershipState.BAN)))
|
|
||||||
val confirmingState = awaitItem()
|
|
||||||
assertThat(confirmingState.unbanUserAsyncAction).isInstanceOf(AsyncAction.Confirming::class.java)
|
|
||||||
confirmingState.eventSink(RoomMembersModerationEvents.UnbanUser(aVictor().userId))
|
|
||||||
assertThat(awaitItem().unbanUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
|
||||||
assertThat(awaitItem().unbanUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java)
|
|
||||||
// Reset it
|
|
||||||
initialItem.eventSink(RoomMembersModerationEvents.Reset)
|
|
||||||
assertThat(awaitItem().unbanUserAsyncAction).isEqualTo(AsyncAction.Uninitialized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun TestScope.createRoomMembersModerationPresenter(
|
|
||||||
joinedRoom: FakeJoinedRoom = aJoinedRoom(),
|
|
||||||
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
|
||||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
|
||||||
): RoomMembersModerationPresenter {
|
|
||||||
return RoomMembersModerationPresenter(
|
|
||||||
room = joinedRoom,
|
|
||||||
dispatchers = dispatchers,
|
|
||||||
analyticsService = analyticsService,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,274 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl.members.moderation
|
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
|
||||||
import androidx.compose.ui.test.performTextInput
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import io.element.android.features.roomdetails.impl.R
|
|
||||||
import io.element.android.features.roomdetails.impl.members.anAlice
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
import io.element.android.libraries.matrix.test.A_REASON
|
|
||||||
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
|
|
||||||
import org.robolectric.annotation.Config
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class RoomMembersModerationViewTest {
|
|
||||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
|
||||||
|
|
||||||
@Ignore("This test is not passing yet, need to investigate")
|
|
||||||
@Test
|
|
||||||
fun `clicking on back emits the expected event`() {
|
|
||||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
|
||||||
val roomMember = anAlice()
|
|
||||||
val state = aRoomMembersModerationState(
|
|
||||||
selectedRoomMember = roomMember,
|
|
||||||
actions = listOf(
|
|
||||||
ModerationAction.DisplayProfile(roomMember.userId),
|
|
||||||
),
|
|
||||||
eventSink = eventsRecorder
|
|
||||||
)
|
|
||||||
rule.setRoomMembersModerationView(
|
|
||||||
state = state,
|
|
||||||
)
|
|
||||||
rule.pressBackKey()
|
|
||||||
// Give time for the bottom sheet to animate
|
|
||||||
rule.mainClock.advanceTimeBy(1_000)
|
|
||||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.Reset)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `clicking on 'See user info' invokes the expected callback`() {
|
|
||||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>(expectEvents = false)
|
|
||||||
val roomMember = anAlice()
|
|
||||||
val state = aRoomMembersModerationState(
|
|
||||||
selectedRoomMember = roomMember,
|
|
||||||
actions = listOf(
|
|
||||||
ModerationAction.DisplayProfile(roomMember.userId),
|
|
||||||
),
|
|
||||||
eventSink = eventsRecorder
|
|
||||||
)
|
|
||||||
ensureCalledOnceWithParam(roomMember.userId) { callback ->
|
|
||||||
rule.setRoomMembersModerationView(
|
|
||||||
state = state,
|
|
||||||
onDisplayMemberProfile = callback
|
|
||||||
)
|
|
||||||
rule.clickOn(CommonStrings.screen_bottom_sheet_manage_room_member_member_user_info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Config(qualifiers = "h1024dp")
|
|
||||||
@Test
|
|
||||||
fun `clicking on 'Remove member' emits the expected event`() {
|
|
||||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
|
||||||
val roomMember = anAlice()
|
|
||||||
val state = aRoomMembersModerationState(
|
|
||||||
selectedRoomMember = roomMember,
|
|
||||||
actions = listOf(
|
|
||||||
ModerationAction.DisplayProfile(roomMember.userId),
|
|
||||||
ModerationAction.KickUser(roomMember.userId),
|
|
||||||
),
|
|
||||||
eventSink = eventsRecorder
|
|
||||||
)
|
|
||||||
rule.setRoomMembersModerationView(
|
|
||||||
state = state,
|
|
||||||
)
|
|
||||||
rule.clickOn(CommonStrings.screen_bottom_sheet_manage_room_member_remove)
|
|
||||||
// Give time for the bottom sheet to animate
|
|
||||||
rule.mainClock.advanceTimeBy(1_000)
|
|
||||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.KickUser)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `cancelling 'Remove member' confirmation emits the expected event`() {
|
|
||||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
|
||||||
val roomMember = anAlice()
|
|
||||||
val state = aRoomMembersModerationState(
|
|
||||||
selectedRoomMember = roomMember,
|
|
||||||
kickUserAsyncAction = AsyncAction.ConfirmingNoParams,
|
|
||||||
eventSink = eventsRecorder
|
|
||||||
)
|
|
||||||
rule.setRoomMembersModerationView(
|
|
||||||
state = state,
|
|
||||||
)
|
|
||||||
// Note: the string key semantics is not perfect here :/
|
|
||||||
rule.clickOn(CommonStrings.action_cancel)
|
|
||||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.Reset)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `confirming 'Remove member' reason edition then validation emits the expected event`() {
|
|
||||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
|
||||||
val roomMember = anAlice()
|
|
||||||
val state = aRoomMembersModerationState(
|
|
||||||
selectedRoomMember = roomMember,
|
|
||||||
kickUserAsyncAction = AsyncAction.ConfirmingNoParams,
|
|
||||||
eventSink = eventsRecorder
|
|
||||||
)
|
|
||||||
rule.setRoomMembersModerationView(
|
|
||||||
state = state,
|
|
||||||
)
|
|
||||||
val reason = rule.activity.getString(CommonStrings.common_reason)
|
|
||||||
rule.onNodeWithText(reason).performTextInput(A_REASON)
|
|
||||||
rule.clickOn(CommonStrings.screen_bottom_sheet_manage_room_member_kick_member_confirmation_action)
|
|
||||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.DoKickUser(reason = A_REASON))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `confirming 'Remove member' confirmation emits the expected event`() {
|
|
||||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
|
||||||
val roomMember = anAlice()
|
|
||||||
val state = aRoomMembersModerationState(
|
|
||||||
selectedRoomMember = roomMember,
|
|
||||||
kickUserAsyncAction = AsyncAction.ConfirmingNoParams,
|
|
||||||
eventSink = eventsRecorder
|
|
||||||
)
|
|
||||||
rule.setRoomMembersModerationView(
|
|
||||||
state = state,
|
|
||||||
)
|
|
||||||
// Note: the string key semantics is not perfect here :/
|
|
||||||
rule.clickOn(CommonStrings.screen_bottom_sheet_manage_room_member_kick_member_confirmation_action)
|
|
||||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.DoKickUser(reason = ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Config(qualifiers = "h1024dp")
|
|
||||||
@Test
|
|
||||||
fun `clicking on 'Remove and ban member' emits the expected event`() {
|
|
||||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
|
||||||
val roomMember = anAlice()
|
|
||||||
val state = aRoomMembersModerationState(
|
|
||||||
selectedRoomMember = roomMember,
|
|
||||||
actions = listOf(
|
|
||||||
ModerationAction.DisplayProfile(roomMember.userId),
|
|
||||||
ModerationAction.KickUser(roomMember.userId),
|
|
||||||
ModerationAction.BanUser(roomMember.userId),
|
|
||||||
),
|
|
||||||
eventSink = eventsRecorder
|
|
||||||
)
|
|
||||||
rule.setRoomMembersModerationView(
|
|
||||||
state = state,
|
|
||||||
)
|
|
||||||
// Note: the string key semantics is not perfect here :/
|
|
||||||
rule.clickOn(R.string.screen_room_member_list_manage_member_remove_confirmation_ban)
|
|
||||||
// Give time for the bottom sheet to animate
|
|
||||||
rule.mainClock.advanceTimeBy(1_000)
|
|
||||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.BanUser)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `cancelling 'Remove and ban member' confirmation emits the expected event`() {
|
|
||||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
|
||||||
val roomMember = anAlice()
|
|
||||||
val state = aRoomMembersModerationState(
|
|
||||||
selectedRoomMember = roomMember,
|
|
||||||
banUserAsyncAction = AsyncAction.ConfirmingNoParams,
|
|
||||||
eventSink = eventsRecorder
|
|
||||||
)
|
|
||||||
rule.setRoomMembersModerationView(
|
|
||||||
state = state,
|
|
||||||
)
|
|
||||||
// Note: the string key semantics is not perfect here :/
|
|
||||||
rule.clickOn(CommonStrings.action_cancel)
|
|
||||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.Reset)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `confirming 'Remove and ban member' reason edition emits the expected event`() {
|
|
||||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
|
||||||
val roomMember = anAlice()
|
|
||||||
val state = aRoomMembersModerationState(
|
|
||||||
selectedRoomMember = roomMember,
|
|
||||||
banUserAsyncAction = AsyncAction.ConfirmingNoParams,
|
|
||||||
eventSink = eventsRecorder
|
|
||||||
)
|
|
||||||
rule.setRoomMembersModerationView(
|
|
||||||
state = state,
|
|
||||||
)
|
|
||||||
val reason = rule.activity.getString(CommonStrings.common_reason)
|
|
||||||
rule.onNodeWithText(reason).performTextInput(A_REASON)
|
|
||||||
rule.clickOn(CommonStrings.screen_bottom_sheet_manage_room_member_ban_member_confirmation_action)
|
|
||||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.DoBanUser(reason = A_REASON))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `confirming 'Remove and ban member' confirmation emits the expected event`() {
|
|
||||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
|
||||||
val roomMember = anAlice()
|
|
||||||
val state = aRoomMembersModerationState(
|
|
||||||
selectedRoomMember = roomMember,
|
|
||||||
banUserAsyncAction = AsyncAction.ConfirmingNoParams,
|
|
||||||
eventSink = eventsRecorder
|
|
||||||
)
|
|
||||||
rule.setRoomMembersModerationView(
|
|
||||||
state = state,
|
|
||||||
)
|
|
||||||
// Note: the string key semantics is not perfect here :/
|
|
||||||
rule.clickOn(CommonStrings.screen_bottom_sheet_manage_room_member_ban_member_confirmation_action)
|
|
||||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.DoBanUser(reason = ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `cancelling 'Unban member' confirmation emits the expected event`() {
|
|
||||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
|
||||||
val roomMember = anAlice()
|
|
||||||
val state = aRoomMembersModerationState(
|
|
||||||
selectedRoomMember = roomMember,
|
|
||||||
unbanUserAsyncAction = ConfirmingRoomMemberAction(roomMember),
|
|
||||||
eventSink = eventsRecorder
|
|
||||||
)
|
|
||||||
rule.setRoomMembersModerationView(
|
|
||||||
state = state,
|
|
||||||
)
|
|
||||||
// Note: the string key semantics is not perfect here :/
|
|
||||||
rule.clickOn(CommonStrings.action_cancel)
|
|
||||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.Reset)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `confirming 'Unban member' confirmation emits the expected event`() {
|
|
||||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
|
||||||
val roomMember = anAlice()
|
|
||||||
val state = aRoomMembersModerationState(
|
|
||||||
selectedRoomMember = roomMember,
|
|
||||||
unbanUserAsyncAction = ConfirmingRoomMemberAction(roomMember),
|
|
||||||
eventSink = eventsRecorder
|
|
||||||
)
|
|
||||||
rule.setRoomMembersModerationView(
|
|
||||||
state = state,
|
|
||||||
)
|
|
||||||
// Note: the string key semantics is not perfect here :/
|
|
||||||
rule.clickOn(R.string.screen_room_member_list_manage_member_unban_action)
|
|
||||||
eventsRecorder.assertSingle(RoomMembersModerationEvents.UnbanUser(roomMember.userId))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomMembersModerationView(
|
|
||||||
state: RoomMembersModerationState,
|
|
||||||
onDisplayMemberProfile: (UserId) -> Unit = EnsureNeverCalledWithParam()
|
|
||||||
) {
|
|
||||||
setContent {
|
|
||||||
RoomMembersModerationView(
|
|
||||||
state = state,
|
|
||||||
onDisplayMemberProfile = onDisplayMemberProfile,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -7,13 +7,10 @@
|
||||||
|
|
||||||
package io.element.android.features.roommembermoderation.impl
|
package io.element.android.features.roommembermoderation.impl
|
||||||
|
|
||||||
import io.element.android.features.roommembermoderation.api.ModerationAction
|
|
||||||
import io.element.android.features.roommembermoderation.api.ModerationActionState
|
import io.element.android.features.roommembermoderation.api.ModerationActionState
|
||||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
|
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
|
||||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,11 @@ import io.element.android.features.roommembermoderation.api.ModerationAction
|
||||||
import io.element.android.features.roommembermoderation.api.ModerationActionState
|
import io.element.android.features.roommembermoderation.api.ModerationActionState
|
||||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
|
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
|
||||||
import io.element.android.libraries.matrix.api.room.toMatrixUser
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
||||||
class RoomMemberModerationStateProvider : PreviewParameterProvider<InternalRoomMemberModerationState> {
|
class InternalRoomMemberModerationStateProvider : PreviewParameterProvider<InternalRoomMemberModerationState> {
|
||||||
override val values: Sequence<InternalRoomMemberModerationState>
|
override val values: Sequence<InternalRoomMemberModerationState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aRoomMembersModerationState(
|
aRoomMembersModerationState(
|
||||||
|
|
@ -317,7 +317,7 @@ private fun RoomMemberActionsBottomSheet(
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RoomMembersModerationViewPreview(@PreviewParameter(RoomMemberModerationStateProvider::class) state: InternalRoomMemberModerationState) {
|
internal fun RoomMemberModerationViewPreview(@PreviewParameter(InternalRoomMemberModerationStateProvider::class) state: InternalRoomMemberModerationState) {
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue