change(members): update tests to match new ui and logic

This commit is contained in:
ganfra 2025-11-25 12:54:35 +01:00
parent 8fd34051e0
commit bf45ed3f1f
3 changed files with 198 additions and 314 deletions

View file

@ -25,7 +25,6 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.map import io.element.android.libraries.architecture.map
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.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
@ -59,9 +58,6 @@ class RoomMemberListPresenter(
@Composable @Composable
override fun present(): RoomMemberListState { override fun present(): RoomMemberListState {
var searchQuery by rememberSaveable { mutableStateOf("") } var searchQuery by rememberSaveable { mutableStateOf("") }
var searchResults by remember {
mutableStateOf<SearchBarResultState<AsyncData<RoomMembers>>>(SearchBarResultState.Initial())
}
val membersState by room.membersStateFlow.collectAsState() val membersState by room.membersStateFlow.collectAsState()
val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val canInvite by room.canInviteAsState(syncUpdateFlow.value) val canInvite by room.canInviteAsState(syncUpdateFlow.value)
@ -75,8 +71,8 @@ class RoomMemberListPresenter(
.launchIn(this) .launchIn(this)
} }
var roomMembers: AsyncData<RoomMembers> by remember { mutableStateOf(AsyncData.Loading()) }
var selectedSection by remember { mutableStateOf(SelectedSection.MEMBERS) } var selectedSection by remember { mutableStateOf(SelectedSection.MEMBERS) }
var roomMembers: AsyncData<RoomMembers> by remember { mutableStateOf(AsyncData.Loading()) }
var filteredRoomMembers: AsyncData<RoomMembers> by remember { mutableStateOf(AsyncData.Loading()) } var filteredRoomMembers: AsyncData<RoomMembers> by remember { mutableStateOf(AsyncData.Loading()) }
// Update the room members when the screen is loaded // Update the room members when the screen is loaded

View file

@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
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.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.architecture.map
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.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
@ -21,108 +21,59 @@ import kotlinx.collections.immutable.persistentListOf
internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> { internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> {
override val values: Sequence<RoomMemberListState> override val values: Sequence<RoomMemberListState>
get() = roomMemberListStates() + bannedRoomMemberListStates() get() = sequenceOf(
aRoomMemberListState(
roomMembers = AsyncData.Loading(),
selectedSection = SelectedSection.MEMBERS,
),
aRoomMemberListState(
roomMembers = AsyncData.Failure(Exception("Error details")),
selectedSection = SelectedSection.MEMBERS,
),
aRoomMemberListState(
roomMembers = aLoadedRoomMembers(),
selectedSection = SelectedSection.MEMBERS,
),
aRoomMemberListState(
roomMembers = aLoadedRoomMembers(),
selectedSection = SelectedSection.BANNED,
moderationState = aRoomMemberModerationState(canBan = true),
),
aRoomMemberListState(
roomMembers = aLoadedRoomMembers(),
canInvite = true,
selectedSection = SelectedSection.MEMBERS,
),
aRoomMemberListState(
roomMembers = aLoadedRoomMembers(),
searchQuery = "alice",
selectedSection = SelectedSection.MEMBERS,
),
aRoomMemberListState(
roomMembers = aLoadedRoomMembers(),
searchQuery = "something-with-no-results",
selectedSection = SelectedSection.MEMBERS,
),
)
} }
private fun roomMemberListStates(): Sequence<RoomMemberListState> = sequenceOf( private fun aLoadedRoomMembers() = AsyncData.Success(
aRoomMemberListState( RoomMembers(
roomMembers = AsyncData.Success( invited = persistentListOf(
RoomMembers( anInvitedVictor().withIdentity(),
invited = persistentListOf(aVictor().withIdentity(), aWalter().withIdentity()), anInvitedWalter().withIdentity(),
joined = persistentListOf(anAlice().withIdentity(), aBob().withIdentity(), aWalter().withIdentity()),
banned = persistentListOf(),
),
), ),
selectedSection = SelectedSection.MEMBERS, joined = persistentListOf(
), anAlice().withIdentity(identityState = IdentityState.Verified),
aRoomMemberListState( aBob().withIdentity(identityState = IdentityState.PinViolation),
roomMembers = AsyncData.Success( aCarol().withIdentity(),
RoomMembers( aDavid().withIdentity(),
invited = persistentListOf(aVictor().withIdentity(), aWalter().withIdentity()), anEve().withIdentity(identityState = IdentityState.VerificationViolation)
joined = persistentListOf(
anAlice().withIdentity(identityState = IdentityState.Verified),
aBob().withIdentity(identityState = IdentityState.PinViolation),
aWalter().withIdentity(identityState = IdentityState.VerificationViolation)
),
banned = persistentListOf(),
)
), ),
selectedSection = SelectedSection.MEMBERS, banned = persistentListOf(
moderationState = aRoomMemberModerationState(canBan = true) aBannedMallory().withIdentity(),
), aBannedSusie().withIdentity()
aRoomMemberListState(
roomMembers = AsyncData.Loading(),
selectedSection = SelectedSection.MEMBERS,
),
aRoomMemberListState().copy(
canInvite = true,
selectedSection = SelectedSection.MEMBERS,
),
aRoomMemberListState().copy(
selectedSection = SelectedSection.MEMBERS,
),
aRoomMemberListState().copy(
selectedSection = SelectedSection.MEMBERS,
),
aRoomMemberListState().copy(
searchQuery = "someone",
selectedSection = SelectedSection.MEMBERS,
),
aRoomMemberListState().copy(
searchQuery = "@someone:matrix.org",
selectedSection = SelectedSection.MEMBERS,
),
aRoomMemberListState().copy(
searchQuery = "something-with-no-results",
selectedSection = SelectedSection.MEMBERS,
),
aRoomMemberListState(
roomMembers = AsyncData.Failure(Exception("Error details")),
selectedSection = SelectedSection.MEMBERS,
),
)
private fun bannedRoomMemberListStates(): Sequence<RoomMemberListState> = sequenceOf(
aRoomMemberListState(
roomMembers = AsyncData.Success(
RoomMembers(
invited = persistentListOf(),
joined = persistentListOf(),
banned = persistentListOf(
aRoomMember(userId = UserId("@alice:example.com"), displayName = "Alice").withIdentity(),
aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob").withIdentity(),
aRoomMember(userId = UserId("@charlie:example.com"), displayName = "Charlie").withIdentity(),
),
)
), ),
moderationState = aRoomMemberModerationState(),
selectedSection = SelectedSection.BANNED,
),
aRoomMemberListState(
roomMembers = AsyncData.Loading(
RoomMembers(
invited = persistentListOf(),
joined = persistentListOf(),
banned = persistentListOf(
aRoomMember(userId = UserId("@alice:example.com"), displayName = "Alice").withIdentity(),
aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob").withIdentity(),
aRoomMember(userId = UserId("@charlie:example.com"), displayName = "Charlie").withIdentity(),
),
)
),
moderationState = aRoomMemberModerationState(),
selectedSection = SelectedSection.BANNED,
),
aRoomMemberListState(
roomMembers = AsyncData.Success(
RoomMembers(
invited = persistentListOf(),
joined = persistentListOf(),
banned = persistentListOf(),
)
),
moderationState = aRoomMemberModerationState(),
selectedSection = SelectedSection.BANNED,
) )
) )
@ -130,14 +81,17 @@ internal fun aRoomMemberListState(
roomMembers: AsyncData<RoomMembers> = AsyncData.Loading(), roomMembers: AsyncData<RoomMembers> = AsyncData.Loading(),
moderationState: RoomMemberModerationState = aRoomMemberModerationState(), moderationState: RoomMemberModerationState = aRoomMemberModerationState(),
selectedSection: SelectedSection = SelectedSection.MEMBERS, selectedSection: SelectedSection = SelectedSection.MEMBERS,
searchQuery: String = "",
canInvite: Boolean = false,
eventSink: (RoomMemberListEvents) -> Unit = {},
) = RoomMemberListState( ) = RoomMemberListState(
roomMembers = roomMembers, roomMembers = roomMembers,
filteredRoomMembers = roomMembers, filteredRoomMembers = roomMembers.map { it.filter(searchQuery) },
searchQuery = "", searchQuery = searchQuery,
canInvite = false, canInvite = canInvite,
moderationState = moderationState, moderationState = moderationState,
selectedSection = selectedSection, selectedSection = selectedSection,
eventSink = {} eventSink = eventSink
) )
fun aRoomMemberModerationState( fun aRoomMemberModerationState(
@ -176,21 +130,30 @@ fun aRoomMember(
fun aRoomMemberList() = persistentListOf( fun aRoomMemberList() = persistentListOf(
anAlice(), anAlice(),
aBob(), aBob(),
aRoomMember(UserId("@carol:server.org"), "Carol"), aCarol(),
aRoomMember(UserId("@david:server.org"), "David"), aDavid(),
aRoomMember(UserId("@eve:server.org"), "Eve"), anEve(),
aRoomMember(UserId("@justin:server.org"), "Justin"), anInvitedVictor(),
aRoomMember(UserId("@mallory:server.org"), "Mallory"), anInvitedWalter(),
aRoomMember(UserId("@susie:server.org"), "Susie"), aBannedSusie(),
aVictor(), aBannedMallory(),
aWalter(),
) )
fun anEve(): RoomMember = aRoomMember(UserId("@eve:server.org"), "Eve")
fun aDavid(): RoomMember = aRoomMember(UserId("@david:server.org"), "David")
fun aCarol(): RoomMember = aRoomMember(UserId("@carol:server.org"), "Carol")
fun anAlice() = aRoomMember(UserId("@alice:server.org"), "Alice", role = RoomMember.Role.Admin) fun anAlice() = aRoomMember(UserId("@alice:server.org"), "Alice", role = RoomMember.Role.Admin)
fun aBob() = aRoomMember(UserId("@bob:server.org"), "Bob", role = RoomMember.Role.Moderator) fun aBob() = aRoomMember(UserId("@bob:server.org"), "Bob", role = RoomMember.Role.Moderator)
fun aVictor() = aRoomMember(UserId("@victor:server.org"), "Victor", membership = RoomMembershipState.INVITE) fun anInvitedVictor() = aRoomMember(UserId("@victor:server.org"), "Victor", membership = RoomMembershipState.INVITE)
fun aWalter() = aRoomMember(UserId("@walter:server.org"), "Walter", membership = RoomMembershipState.INVITE) fun anInvitedWalter() = aRoomMember(UserId("@walter:server.org"), "Walter", membership = RoomMembershipState.INVITE)
fun aBannedSusie(): RoomMember = aRoomMember(UserId("@susie:server.org"), "Susie", membership = RoomMembershipState.BAN)
fun aBannedMallory(): RoomMember = aRoomMember(UserId("@mallory:server.org"), "Mallory", membership = RoomMembershipState.BAN)
private fun RoomMember.withIdentity(identityState: IdentityState? = null) = RoomMemberWithIdentityState(this, identityState) private fun RoomMember.withIdentity(identityState: IdentityState? = null) = RoomMemberWithIdentityState(this, identityState)

View file

@ -8,36 +8,27 @@
package io.element.android.features.roomdetails.impl.members package io.element.android.features.roomdetails.impl.members
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
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.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.RoomMembersState 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.encryption.FakeEncryptionService 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.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.time.withTimeout
import kotlinx.coroutines.withTimeout
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import kotlin.time.Duration.Companion.seconds
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
class RoomMemberListPresenterTest { class RoomMemberListPresenterTest {
@ -45,176 +36,132 @@ class RoomMemberListPresenterTest {
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
@Test @Test
fun `member loading is done automatically on start, but is async`() = runTest { fun `initial state is loading`() = runTest {
val room = FakeJoinedRoom( val presenter = createPresenter()
baseRoom = FakeBaseRoom( presenter.test {
updateMembersResult = { Result.success(Unit) },
canInviteResult = { Result.success(true) }
).apply {
// Needed to avoid discarding the loaded members as a partial and invalid result
givenRoomInfo(aRoomInfo(joinedMembersCount = 2))
}
)
val presenter = createPresenter(joinedRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.roomMembers.isLoading()).isTrue() assertThat(initialState.filteredRoomMembers.isLoading()).isTrue()
assertThat(initialState.searchQuery).isEmpty() assertThat(initialState.searchQuery).isEmpty()
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java) assertThat(initialState.selectedSection).isEqualTo(SelectedSection.MEMBERS)
assertThat(initialState.isSearchActive).isFalse()
room.givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
// Skip item while the new members state is processed
skipItems(1)
val loadedMembersState = awaitItem()
assertThat(loadedMembersState.roomMembers.isLoading()).isFalse()
assertThat(loadedMembersState.roomMembers.dataOrNull()?.invited)
.isEqualTo(listOf(RoomMemberWithIdentityState(aVictor(), null), RoomMemberWithIdentityState(aWalter(), null)))
assertThat(loadedMembersState.roomMembers.dataOrNull()?.joined).isNotEmpty()
} }
} }
@Test @Test
fun `member loading is done automatically when RoomInfo's activeMemberCount changes`() = runTest { fun `hide banned section when there is no banned users`() = runTest {
val reloadMembersMutex = Mutex() val allRoomMembers = aRoomMemberList()
val updateMembersLambda = lambdaRecorder<Unit> { val noBannedMembers = allRoomMembers
if (reloadMembersMutex.isLocked) { .filterNot { it.membership == RoomMembershipState.BAN }
reloadMembersMutex.unlock() .toImmutableList()
val room = createFakeJoinedRoom()
.apply {
givenRoomMembersState(RoomMembersState.Ready(allRoomMembers))
} }
}
val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
updateMembersResult = updateMembersLambda,
canInviteResult = { Result.success(true) }
).apply {
// Needed to avoid discarding the loaded members as a partial and invalid result
givenRoomInfo(aRoomInfo(joinedMembersCount = 2))
}
)
val presenter = createPresenter(joinedRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.roomMembers.isLoading()).isTrue()
room.givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
// Skip item while the new members state is processed
skipItems(1)
val loadedMembersState = awaitItem()
assertThat(loadedMembersState.roomMembers.isLoading()).isFalse()
assertThat(loadedMembersState.roomMembers.dataOrNull()?.joined).isNotEmpty()
// Assert no events are emitted only with that change
expectNoEvents()
// This will only progress if the `Room.updateMembers()` function is called, triggered by the RoomInfo change
withTimeout(10.seconds) {
reloadMembersMutex.withLock {
launch { room.givenRoomInfo(aRoomInfo(activeMembersCount = 0L)) }
}
}
// Update the room members state as `Room.updateMembers()` would have done with the actual implementation
room.givenRoomMembersState(RoomMembersState.Ready(persistentListOf()))
// Wait for another update
skipItems(1)
// The members should be reloaded now
assertThat(awaitItem().roomMembers.dataOrNull()?.joined).isEmpty()
}
}
@Test
fun `open search`() = runTest {
val presenter = createPresenter( val presenter = createPresenter(
joinedRoom = FakeJoinedRoom( joinedRoom = room,
baseRoom = FakeBaseRoom( roomMemberModerationState = aRoomMemberModerationState(canBan = true),
updateMembersResult = { Result.success(Unit) },
canInviteResult = { Result.success(true) }
)
)
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
skipItems(1) skipItems(1)
val loadedState = awaitItem() val loadedState = awaitItem()
loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true)) assertThat(loadedState.showBannedSection).isTrue()
loadedState.eventSink(RoomMemberListEvents.ChangeSelectedSection(SelectedSection.BANNED))
val bannedSectionState = awaitItem()
assertThat(bannedSectionState.selectedSection).isEqualTo(SelectedSection.BANNED)
// Now update the room members to have no banned users
room.givenRoomMembersState(RoomMembersState.Ready(noBannedMembers))
skipItems(1) skipItems(1)
val searchActiveState = awaitItem() val noBannedMembersState = awaitItem()
assertThat(searchActiveState.isSearchActive).isTrue() assertThat(noBannedMembersState.showBannedSection).isFalse()
skipItems(1)
val finalState = awaitItem()
assertThat(finalState.selectedSection).isEqualTo(SelectedSection.MEMBERS)
}
}
@Test
fun `member loading is done automatically on start, but is async`() = runTest {
val room = createFakeJoinedRoom()
val presenter = createPresenter(joinedRoom = room)
presenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.filteredRoomMembers.isLoading()).isTrue()
assertThat(initialState.searchQuery).isEmpty()
room.givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
// Skip items while the new members state is processed
skipItems(2)
val loadedState = awaitItem()
val loadedRoomMembers = loadedState.filteredRoomMembers.dataOrNull()!!
assertThat(loadedRoomMembers.joined).isNotEmpty()
assertThat(loadedRoomMembers.banned).isNotEmpty()
assertThat(loadedRoomMembers.invited).isNotEmpty()
assertThat(loadedRoomMembers.isEmpty(SelectedSection.MEMBERS)).isFalse()
assertThat(loadedRoomMembers.isEmpty(SelectedSection.BANNED)).isFalse()
} }
} }
@Test @Test
fun `search for something which is not found`() = runTest { fun `search for something which is not found`() = runTest {
val presenter = createPresenter( val room = createFakeJoinedRoom().apply {
joinedRoom = FakeJoinedRoom( givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
baseRoom = FakeBaseRoom( }
updateMembersResult = { Result.success(Unit) }, val presenter = createPresenter(joinedRoom = room)
canInviteResult = { Result.success(true) } presenter.test {
)
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1) skipItems(1)
val loadedState = awaitItem() val loadedState = awaitItem()
loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true)) val loadedRoomMembers = loadedState.filteredRoomMembers.dataOrNull()!!
val searchActiveState = awaitItem() assertThat(loadedRoomMembers.joined).isNotEmpty()
searchActiveState.eventSink(RoomMemberListEvents.UpdateSearchQuery("something")) assertThat(loadedRoomMembers.banned).isNotEmpty()
skipItems(1) assertThat(loadedRoomMembers.invited).isNotEmpty()
assertThat(loadedRoomMembers.isEmpty(SelectedSection.MEMBERS)).isFalse()
assertThat(loadedRoomMembers.isEmpty(SelectedSection.BANNED)).isFalse()
loadedState.eventSink(RoomMemberListEvents.UpdateSearchQuery("something"))
val searchQueryUpdatedState = awaitItem() val searchQueryUpdatedState = awaitItem()
assertThat(searchQueryUpdatedState.searchQuery).isEqualTo("something") assertThat(searchQueryUpdatedState.searchQuery).isEqualTo("something")
val searchSearchResultDelivered = awaitItem() val searchSearchResultDelivered = awaitItem()
assertThat(searchSearchResultDelivered.searchResults).isInstanceOf(SearchBarResultState.NoResultsFound::class.java) val emptyRoomMembers = searchSearchResultDelivered.filteredRoomMembers.dataOrNull()!!
assertThat(emptyRoomMembers.joined).isEmpty()
assertThat(emptyRoomMembers.banned).isEmpty()
assertThat(emptyRoomMembers.invited).isEmpty()
assertThat(emptyRoomMembers.isEmpty(SelectedSection.MEMBERS)).isTrue()
assertThat(emptyRoomMembers.isEmpty(SelectedSection.BANNED)).isTrue()
} }
} }
@Test @Test
fun `search for something which is found`() = runTest { fun `search for something which is found`() = runTest {
val presenter = createPresenter( val room = createFakeJoinedRoom().apply {
joinedRoom = FakeJoinedRoom( givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
baseRoom = FakeBaseRoom( }
updateMembersResult = { Result.success(Unit) }, val presenter = createPresenter(joinedRoom = room)
canInviteResult = { Result.success(true) } presenter.test {
)
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1) skipItems(1)
val loadedState = awaitItem() val loadedState = awaitItem()
loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true)) val loadedRoomMembers = loadedState.filteredRoomMembers.dataOrNull()!!
val searchActiveState = awaitItem() assertThat(loadedRoomMembers.joined).isNotEmpty()
searchActiveState.eventSink(RoomMemberListEvents.UpdateSearchQuery("Alice")) assertThat(loadedRoomMembers.banned).isNotEmpty()
skipItems(1) assertThat(loadedRoomMembers.invited).isNotEmpty()
assertThat(loadedRoomMembers.isEmpty(SelectedSection.MEMBERS)).isFalse()
assertThat(loadedRoomMembers.isEmpty(SelectedSection.BANNED)).isFalse()
loadedState.eventSink(RoomMemberListEvents.UpdateSearchQuery("alice"))
val searchQueryUpdatedState = awaitItem() val searchQueryUpdatedState = awaitItem()
assertThat(searchQueryUpdatedState.searchQuery).isEqualTo("Alice") assertThat(searchQueryUpdatedState.searchQuery).isEqualTo("alice")
val searchSearchResultDelivered = awaitItem() val searchSearchResultDelivered = awaitItem()
assertThat(searchSearchResultDelivered.searchResults).isInstanceOf(SearchBarResultState.Results::class.java) val emptyRoomMembers = searchSearchResultDelivered.filteredRoomMembers.dataOrNull()!!
assertThat((searchSearchResultDelivered.searchResults as SearchBarResultState.Results).results.dataOrNull()!!.joined.first().roomMember.displayName) assertThat(emptyRoomMembers.joined).isNotEmpty()
.isEqualTo("Alice") assertThat(emptyRoomMembers.banned).isEmpty()
assertThat(emptyRoomMembers.invited).isEmpty()
assertThat(emptyRoomMembers.isEmpty(SelectedSection.MEMBERS)).isFalse()
assertThat(emptyRoomMembers.isEmpty(SelectedSection.BANNED)).isTrue()
} }
} }
@Test @Test
fun `present - asynchronously sets canInvite when user has correct power level`() = runTest { fun `present - asynchronously sets canInvite when user has correct power level`() = runTest {
val presenter = createPresenter( val presenter = createPresenter()
joinedRoom = FakeJoinedRoom( presenter.test {
baseRoom = FakeBaseRoom(
canInviteResult = { Result.success(true) },
updateMembersResult = { Result.success(Unit) }
)
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1) skipItems(1)
val loadedState = awaitItem() val loadedState = awaitItem()
assertThat(loadedState.canInvite).isTrue() assertThat(loadedState.canInvite).isTrue()
@ -224,17 +171,11 @@ class RoomMemberListPresenterTest {
@Test @Test
fun `present - asynchronously sets canInvite when user does not have correct power level`() = runTest { fun `present - asynchronously sets canInvite when user does not have correct power level`() = runTest {
val presenter = createPresenter( val presenter = createPresenter(
joinedRoom = FakeJoinedRoom( joinedRoom = createFakeJoinedRoom(
baseRoom = FakeBaseRoom( canInviteResult = { Result.success(false) },
canInviteResult = { Result.success(false) },
updateMembersResult = { Result.success(Unit) }
)
) )
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
skipItems(1)
val loadedState = awaitItem() val loadedState = awaitItem()
assertThat(loadedState.canInvite).isFalse() assertThat(loadedState.canInvite).isFalse()
} }
@ -243,70 +184,54 @@ class RoomMemberListPresenterTest {
@Test @Test
fun `present - asynchronously sets canInvite when power level check fails`() = runTest { fun `present - asynchronously sets canInvite when power level check fails`() = runTest {
val presenter = createPresenter( val presenter = createPresenter(
joinedRoom = FakeJoinedRoom( joinedRoom = createFakeJoinedRoom(
baseRoom = FakeBaseRoom( canInviteResult = { Result.failure(RuntimeException("Eek")) },
canInviteResult = { Result.failure(RuntimeException("Eek")) },
updateMembersResult = { Result.success(Unit) }
)
) )
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
skipItems(1)
val loadedState = awaitItem() val loadedState = awaitItem()
assertThat(loadedState.canInvite).isFalse() assertThat(loadedState.canInvite).isFalse()
} }
} }
@Test @Test
fun `present - RoomMemberSelected will open the moderation options when target user is not banned`() = runTest { fun `present - RoomMemberSelected will open the moderation options`() = runTest {
val roomMemberModerationPresenter = Presenter {
aRoomMemberModerationState(canBan = true, canKick = true)
}
val presenter = createPresenter( val presenter = createPresenter(
roomMemberModerationPresenter = roomMemberModerationPresenter, roomMemberModerationState = aRoomMemberModerationState(canBan = true, canKick = true)
joinedRoom = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
updateMembersResult = { Result.success(Unit) },
canInviteResult = { Result.success(true) }
)
)
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
skipItems(1) skipItems(1)
awaitItem().eventSink(RoomMemberListEvents.RoomMemberSelected(aVictor())) awaitItem().eventSink(RoomMemberListEvents.RoomMemberSelected(anInvitedVictor()))
} }
} }
} }
@ExperimentalCoroutinesApi private fun createFakeJoinedRoom(
private fun TestScope.createDataSource( updateMembersResult: () -> Unit = { },
room: BaseRoom = FakeBaseRoom().apply { canInviteResult: (UserId) -> Result<Boolean> = { Result.success(true) },
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) ): FakeJoinedRoom {
}, return FakeJoinedRoom(
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers() baseRoom = FakeBaseRoom(
) = RoomMemberListDataSource(room, coroutineDispatchers) updateMembersResult = updateMembersResult,
canInviteResult = canInviteResult,
).apply {
// Needed to avoid discarding the loaded members as a partial and invalid result
givenRoomInfo(aRoomInfo(joinedMembersCount = 2))
}
)
}
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
private fun TestScope.createPresenter( private fun TestScope.createPresenter(
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
joinedRoom: JoinedRoom = FakeJoinedRoom( joinedRoom: JoinedRoom = createFakeJoinedRoom(),
baseRoom = FakeBaseRoom(
updateMembersResult = { Result.success(Unit) }
)
),
roomMemberListDataSource: RoomMemberListDataSource = createDataSource(coroutineDispatchers = coroutineDispatchers),
encryptedService: FakeEncryptionService = FakeEncryptionService(), encryptedService: FakeEncryptionService = FakeEncryptionService(),
roomMemberModerationPresenter: Presenter<RoomMemberModerationState> = Presenter { roomMemberModerationState: RoomMemberModerationState = aRoomMemberModerationState(),
aRoomMemberModerationState()
},
) = RoomMemberListPresenter( ) = RoomMemberListPresenter(
room = joinedRoom, room = joinedRoom,
roomMemberListDataSource = roomMemberListDataSource,
coroutineDispatchers = coroutineDispatchers, coroutineDispatchers = coroutineDispatchers,
roomMembersModerationPresenter = roomMemberModerationPresenter, roomMembersModerationPresenter = Presenter {
roomMemberModerationState
},
encryptionService = encryptedService, encryptionService = encryptedService,
) )