From efdf2020fe634f73872c85860e09276bc2fef991 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 14 Jan 2026 17:47:20 +0100 Subject: [PATCH] Add tests for space manage rooms mode --- .../space/impl/root/SpacePresenterTest.kt | 211 +++++++++++++++++- .../space/impl/root/SpaceStateTest.kt | 78 +++++++ .../features/space/impl/root/SpaceViewTest.kt | 68 ++++++ 3 files changed, 356 insertions(+), 1 deletion(-) diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt index 65a4c76783..08fb977d1b 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt @@ -22,16 +22,18 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import io.element.android.libraries.matrix.api.room.RoomType import io.element.android.libraries.matrix.api.room.join.JoinRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoomList -import io.element.android.libraries.matrix.api.spaces.SpaceService import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID_2 +import io.element.android.libraries.matrix.test.A_ROOM_ID_3 import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom @@ -355,6 +357,213 @@ class SpacePresenterTest { } } + @Test + fun `present - enter manage mode`() = runTest { + val presenter = createSpacePresenter() + presenter.test { + val state = awaitItem() + assertThat(state.isManageMode).isFalse() + state.eventSink(SpaceEvents.EnterManageMode) + val manageModeState = awaitItem() + assertThat(manageModeState.isManageMode).isTrue() + assertThat(manageModeState.selectedRoomIds).isEmpty() + } + } + + @Test + fun `present - exit manage mode clears selection`() = runTest { + val presenter = createSpacePresenter() + presenter.test { + val initialState = awaitItem() + initialState.eventSink(SpaceEvents.EnterManageMode) + initialState.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) + initialState.eventSink(SpaceEvents.ExitManageMode) + val finalState = expectMostRecentItem() + assertThat(finalState.isManageMode).isFalse() + assertThat(finalState.selectedRoomIds).isEmpty() + } + } + + @Test + fun `present - toggle room selection`() = runTest { + val presenter = createSpacePresenter() + presenter.test { + val initialState = awaitItem() + initialState.eventSink(SpaceEvents.EnterManageMode) + // Select a room + initialState.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) + var latestState = expectMostRecentItem() + assertThat(latestState.selectedRoomIds).containsExactly(A_ROOM_ID) + // Deselect the room + latestState.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) + latestState = expectMostRecentItem() + assertThat(latestState.selectedRoomIds).isEmpty() + } + } + + @Test + fun `present - remove rooms success`() = runTest { + val removeChildFromSpaceResult = lambdaRecorder> { _, _ -> + Result.success(Unit) + } + val aRoom = aSpaceRoom( + roomId = A_ROOM_ID, + roomType = RoomType.Room, + ) + val fakeSpaceRoomList = FakeSpaceRoomList( + initialSpaceRoomsValue = listOf(aRoom), + paginateResult = { Result.success(Unit) }, + ) + val presenter = createSpacePresenter( + spaceRoomList = fakeSpaceRoomList, + spaceService = FakeSpaceService( + removeChildFromSpaceResult = removeChildFromSpaceResult, + ), + ) + presenter.test { + awaitItem() // Initial empty state + advanceUntilIdle() + val stateWithChildren = awaitItem() + assertThat(stateWithChildren.children).hasSize(1) + stateWithChildren.eventSink(SpaceEvents.EnterManageMode) + stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) + stateWithChildren.eventSink(SpaceEvents.RemoveSelectedRooms) + stateWithChildren.eventSink(SpaceEvents.ConfirmRoomRemoval) + advanceUntilIdle() + val successState = expectMostRecentItem() + assertThat(successState.removeRoomsAction).isEqualTo(AsyncAction.Success(Unit)) + assertThat(successState.isManageMode).isFalse() + assertThat(successState.children).isEmpty() + removeChildFromSpaceResult.assertions().isCalledOnce() + } + } + + @Test + fun `present - remove rooms partial failure`() = runTest { + val aRoom1 = aSpaceRoom( + roomId = A_ROOM_ID, + roomType = RoomType.Room, + ) + val aRoom2 = aSpaceRoom( + roomId = A_ROOM_ID_2, + roomType = RoomType.Room, + ) + val removeChildFromSpaceResult = lambdaRecorder> { _, childId -> + if (childId == A_ROOM_ID_2) Result.failure(AN_EXCEPTION) + else Result.success(Unit) + } + val fakeSpaceRoomList = FakeSpaceRoomList( + initialSpaceRoomsValue = listOf(aRoom1, aRoom2), + paginateResult = { Result.success(Unit) }, + ) + val presenter = createSpacePresenter( + spaceRoomList = fakeSpaceRoomList, + spaceService = FakeSpaceService( + removeChildFromSpaceResult = removeChildFromSpaceResult, + ), + ) + presenter.test { + awaitItem() // Initial empty state + advanceUntilIdle() + val stateWithChildren = awaitItem() + assertThat(stateWithChildren.children).hasSize(2) + stateWithChildren.eventSink(SpaceEvents.EnterManageMode) + stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) + stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID_2)) + stateWithChildren.eventSink(SpaceEvents.RemoveSelectedRooms) + stateWithChildren.eventSink(SpaceEvents.ConfirmRoomRemoval) + advanceUntilIdle() + val failureState = expectMostRecentItem() + assertThat(failureState.removeRoomsAction.isFailure()).isTrue() + // Successfully removed room should be filtered out + assertThat(failureState.children.map { it.roomId }).doesNotContain(A_ROOM_ID) + // Failed room should still be present + assertThat(failureState.children.map { it.roomId }).contains(A_ROOM_ID_2) + removeChildFromSpaceResult.assertions().isCalledExactly(2) + } + } + + @Test + fun `present - children filtered in manage mode shows only rooms`() = runTest { + val aRoom = aSpaceRoom( + roomId = A_ROOM_ID, + roomType = RoomType.Room, + ) + val aSubSpace = aSpaceRoom( + roomId = A_ROOM_ID_2, + roomType = RoomType.Space, + ) + val fakeSpaceRoomList = FakeSpaceRoomList( + initialSpaceRoomsValue = listOf(aRoom, aSubSpace), + paginateResult = { Result.success(Unit) }, + ) + val presenter = createSpacePresenter(spaceRoomList = fakeSpaceRoomList) + presenter.test { + awaitItem() // Initial empty state + advanceUntilIdle() + val stateWithChildren = awaitItem() + // Both room and space visible initially + assertThat(stateWithChildren.children).hasSize(2) + assertThat(stateWithChildren.isManageMode).isFalse() + stateWithChildren.eventSink(SpaceEvents.EnterManageMode) + val manageModeState = expectMostRecentItem() + // Only rooms visible in manage mode + assertThat(manageModeState.children).hasSize(1) + assertThat(manageModeState.children.first().roomId).isEqualTo(A_ROOM_ID) + assertThat(manageModeState.children.first().isSpace).isFalse() + } + } + + @Test + fun `present - removed rooms persist after flow update`() = runTest { + val removeChildFromSpaceResult = lambdaRecorder> { _, _ -> + Result.success(Unit) + } + val aRoom1 = aSpaceRoom( + roomId = A_ROOM_ID, + roomType = RoomType.Room, + ) + val aRoom2 = aSpaceRoom( + roomId = A_ROOM_ID_2, + roomType = RoomType.Room, + ) + val aRoom3 = aSpaceRoom( + roomId = A_ROOM_ID_3, + roomType = RoomType.Room, + ) + val spaceRoomList = FakeSpaceRoomList( + initialSpaceRoomsValue = listOf(aRoom1, aRoom2), + paginateResult = { Result.success(Unit) }, + ) + val presenter = createSpacePresenter( + spaceRoomList = spaceRoomList, + spaceService = FakeSpaceService( + removeChildFromSpaceResult = removeChildFromSpaceResult, + ), + ) + presenter.test { + awaitItem() // Initial empty state + advanceUntilIdle() + val stateWithChildren = awaitItem() + stateWithChildren.eventSink(SpaceEvents.EnterManageMode) + stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) + stateWithChildren.eventSink(SpaceEvents.RemoveSelectedRooms) + stateWithChildren.eventSink(SpaceEvents.ConfirmRoomRemoval) + advanceUntilIdle() + val successState = expectMostRecentItem() + assertThat(successState.children.map { it.roomId }).doesNotContain(A_ROOM_ID) + // Emit new flow update with a new room added (simulating server refresh) + spaceRoomList.emitSpaceRooms(listOf(aRoom1, aRoom2, aRoom3)) + advanceUntilIdle() + val afterFlowUpdate = awaitItem() + // A_ROOM_ID should still be filtered out even though it's in the new emission + assertThat(afterFlowUpdate.children.map { it.roomId }).doesNotContain(A_ROOM_ID) + // But the other rooms should be present + assertThat(afterFlowUpdate.children.map { it.roomId }).contains(A_ROOM_ID_2) + assertThat(afterFlowUpdate.children.map { it.roomId }).contains(A_ROOM_ID_3) + } + } + private fun TestScope.createSpacePresenter( client: MatrixClient = FakeMatrixClient(), room: BaseRoom = FakeBaseRoom(), diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt index 440ec1b6a5..a0c3635baf 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt @@ -14,6 +14,8 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_ROOM_ID_3 +import io.element.android.libraries.matrix.api.room.RoomType +import io.element.android.libraries.previewutils.room.aSpaceRoom import org.junit.Test class SpaceStateTest { @@ -45,4 +47,80 @@ class SpaceStateTest { ) assertThat(state.isJoining(A_ROOM_ID)).isTrue() } + + @Test + fun `test isSelected returns true for selected room`() { + val state = aSpaceState( + selectedRoomIds = setOf(A_ROOM_ID) + ) + assertThat(state.isSelected(A_ROOM_ID)).isTrue() + } + + @Test + fun `test isSelected returns false for non-selected room`() { + val state = aSpaceState( + selectedRoomIds = setOf(A_ROOM_ID) + ) + assertThat(state.isSelected(A_ROOM_ID_2)).isFalse() + } + + @Test + fun `test showManageRoomsAction true when canManageRooms and has room children`() { + val state = aSpaceState( + canManageRooms = true, + children = listOf(aSpaceRoom(roomType = RoomType.Room)) + ) + assertThat(state.showManageRoomsAction).isTrue() + } + + @Test + fun `test showManageRoomsAction false when canManageRooms but children empty`() { + val state = aSpaceState( + canManageRooms = true, + children = emptyList() + ) + assertThat(state.showManageRoomsAction).isFalse() + } + + @Test + fun `test showManageRoomsAction false when canManageRooms but only space children`() { + val state = aSpaceState( + canManageRooms = true, + children = listOf(aSpaceRoom(roomType = RoomType.Space)) + ) + assertThat(state.showManageRoomsAction).isFalse() + } + + @Test + fun `test showManageRoomsAction false when has room children but canManageRooms false`() { + val state = aSpaceState( + canManageRooms = false, + children = listOf(aSpaceRoom(roomType = RoomType.Room)) + ) + assertThat(state.showManageRoomsAction).isFalse() + } + + @Test + fun `test selectedCount returns correct count`() { + val state = aSpaceState( + selectedRoomIds = setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3) + ) + assertThat(state.selectedCount).isEqualTo(3) + } + + @Test + fun `test isRemoveButtonEnabled true when selectedRoomIds not empty`() { + val state = aSpaceState( + selectedRoomIds = setOf(A_ROOM_ID) + ) + assertThat(state.isRemoveButtonEnabled).isTrue() + } + + @Test + fun `test isRemoveButtonEnabled false when selectedRoomIds empty`() { + val state = aSpaceState( + selectedRoomIds = emptySet() + ) + assertThat(state.isRemoveButtonEnabled).isFalse() + } } diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt index 406b5d17e8..3ef5151a50 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt @@ -12,9 +12,11 @@ import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -29,6 +31,7 @@ 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 io.element.android.tests.testutils.pressBackKey import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule @@ -132,6 +135,71 @@ class SpaceViewTest { rule.onNodeWithText(A_ROOM_TOPIC).performClick() eventsRecorder.assertSingle(SpaceEvents.ShowTopicViewer(A_ROOM_TOPIC)) } + + @Test + fun `clicking back in manage mode emits ExitManageMode event`() { + val eventsRecorder = EventsRecorder() + rule.setSpaceView( + aSpaceState( + hasMoreToLoad = false, + isManageMode = true, + eventSink = eventsRecorder, + ) + ) + rule.pressBackKey() + eventsRecorder.assertSingle(SpaceEvents.ExitManageMode) + } + + @Test + fun `clicking on room in manage mode emits ToggleRoomSelection event`() { + val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, displayName = A_ROOM_NAME) + val eventsRecorder = EventsRecorder() + rule.setSpaceView( + aSpaceState( + children = listOf(aSpaceRoom), + hasMoreToLoad = false, + isManageMode = true, + eventSink = eventsRecorder, + ) + ) + rule.onNodeWithText(A_ROOM_NAME).performClick() + eventsRecorder.assertSingle(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) + } + + @Test + fun `clicking remove button emits RemoveSelectedRooms event`() { + val eventsRecorder = EventsRecorder() + rule.setSpaceView( + aSpaceState( + children = listOf(aSpaceRoom(roomId = A_ROOM_ID)), + hasMoreToLoad = false, + isManageMode = true, + selectedRoomIds = setOf(A_ROOM_ID), + eventSink = eventsRecorder, + ) + ) + rule.clickOn(CommonStrings.action_remove) + eventsRecorder.assertSingle(SpaceEvents.RemoveSelectedRooms) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking confirm in removal dialog emits ConfirmRoomRemoval event`() { + val eventsRecorder = EventsRecorder() + rule.setSpaceView( + aSpaceState( + children = listOf(aSpaceRoom(roomId = A_ROOM_ID)), + hasMoreToLoad = false, + isManageMode = true, + selectedRoomIds = setOf(A_ROOM_ID), + removeRoomsAction = AsyncAction.ConfirmingNoParams, + eventSink = eventsRecorder, + ) + ) + // Click on the Remove button in the confirmation dialog + rule.clickOn(CommonStrings.action_remove, inDialog = true) + eventsRecorder.assertSingle(SpaceEvents.ConfirmRoomRemoval) + } } private fun AndroidComposeTestRule.setSpaceView(