Add tests to AddRoomToSpace feature

This commit is contained in:
ganfra 2026-01-22 10:24:09 +01:00
parent d45d1e0327
commit df62694b2f
3 changed files with 464 additions and 3 deletions

View file

@ -60,13 +60,14 @@ internal class AddRoomToSpaceStateProvider : PreviewParameterProvider<AddRoomToS
)
}
private fun anAddRoomToSpaceState(
internal fun anAddRoomToSpaceState(
searchQuery: String = "",
searchResults: SearchBarResultState<ImmutableList<SelectRoomInfo>> = SearchBarResultState.Initial(),
selectedRooms: ImmutableList<SelectRoomInfo> = persistentListOf(),
isSearchActive: Boolean = false,
saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
suggestions: ImmutableList<SelectRoomInfo> = persistentListOf(),
eventSink: (AddRoomToSpaceEvent) -> Unit = {},
): AddRoomToSpaceState {
return AddRoomToSpaceState(
searchQuery = searchQuery,
@ -75,11 +76,11 @@ private fun anAddRoomToSpaceState(
isSearchActive = isSearchActive,
saveAction = saveAction,
suggestions = suggestions,
eventSink = {},
eventSink = eventSink,
)
}
private fun aSelectRoomInfoList(): ImmutableList<SelectRoomInfo> = listOf(
internal fun aSelectRoomInfoList(): ImmutableList<SelectRoomInfo> = listOf(
SelectRoomInfo(
roomId = RoomId("!room1:server.org"),
name = "General",

View file

@ -0,0 +1,336 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 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.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.space.impl.addroom
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
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.AN_EXCEPTION
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
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.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Test
class AddRoomToSpacePresenterTest {
@Test
fun `present - initial state has empty selection and no search`() = runTest {
val presenter = createPresenter()
presenter.test {
val state = awaitItem()
assertThat(state.selectedRooms).isEmpty()
assertThat(state.searchQuery).isEmpty()
assertThat(state.isSearchActive).isFalse()
assertThat(state.saveAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(state.canSave).isFalse()
}
}
@Test
fun `present - ToggleRoom adds room to selection`() = runTest {
val presenter = createPresenter()
presenter.test {
val state = awaitItem()
val room = aSelectRoomInfoList().first()
state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room))
val updatedState = awaitItem()
assertThat(updatedState.selectedRooms).hasSize(1)
assertThat(updatedState.selectedRooms.first().roomId).isEqualTo(room.roomId)
assertThat(updatedState.canSave).isTrue()
}
}
@Test
fun `present - ToggleRoom removes already selected room`() = runTest {
val presenter = createPresenter()
presenter.test {
val state = awaitItem()
val room = aSelectRoomInfoList().first()
// Add room
state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room))
val stateWithRoom = awaitItem()
assertThat(stateWithRoom.selectedRooms).hasSize(1)
// Remove room
stateWithRoom.eventSink(AddRoomToSpaceEvent.ToggleRoom(room))
val stateWithoutRoom = awaitItem()
assertThat(stateWithoutRoom.selectedRooms).isEmpty()
assertThat(stateWithoutRoom.canSave).isFalse()
}
}
@Test
fun `present - UpdateSearchQuery updates query`() = runTest {
val presenter = createPresenter()
presenter.test {
val state = awaitItem()
state.eventSink(AddRoomToSpaceEvent.UpdateSearchQuery("test"))
val updatedState = awaitItem()
assertThat(updatedState.searchQuery).isEqualTo("test")
}
}
@Test
fun `present - OnSearchActiveChanged activates search`() = runTest {
val presenter = createPresenter()
presenter.test {
val state = awaitItem()
state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(true))
val updatedState = awaitItem()
assertThat(updatedState.isSearchActive).isTrue()
}
}
@Test
fun `present - OnSearchActiveChanged deactivates search and clears query`() = runTest {
val presenter = createPresenter()
presenter.test {
val state = awaitItem()
// Activate search and set query
state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(true))
awaitItem()
state.eventSink(AddRoomToSpaceEvent.UpdateSearchQuery("test"))
awaitItem()
// Deactivate search
state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(false))
advanceUntilIdle()
val finalState = expectMostRecentItem()
assertThat(finalState.isSearchActive).isFalse()
assertThat(finalState.searchQuery).isEmpty()
}
}
@Test
fun `present - CloseSearch deactivates and clears query`() = runTest {
val presenter = createPresenter()
presenter.test {
val state = awaitItem()
// Activate search and set query
state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(true))
awaitItem()
state.eventSink(AddRoomToSpaceEvent.UpdateSearchQuery("test"))
awaitItem()
// Close search
state.eventSink(AddRoomToSpaceEvent.CloseSearch)
advanceUntilIdle()
val finalState = expectMostRecentItem()
assertThat(finalState.isSearchActive).isFalse()
assertThat(finalState.searchQuery).isEmpty()
}
}
@Test
fun `present - searchResults shows Results when rooms available`() = runTest {
val roomListService = FakeRoomListService()
val presenter = createPresenter(roomListService = roomListService)
presenter.test {
awaitItem() // Initial state
// Post rooms to the service
roomListService.postAllRooms(
listOf(
aRoomSummary(
roomId = A_ROOM_ID,
name = "Room 1",
isDirect = false,
isSpace = false,
currentUserMembership = CurrentUserMembership.JOINED,
)
)
)
advanceUntilIdle()
val state = expectMostRecentItem()
assertThat(state.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
}
}
@Test
fun `present - searchResults shows NoResultsFound when search active with query but no results`() = runTest {
val presenter = createPresenter()
presenter.test {
val state = awaitItem()
state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(true))
awaitItem()
state.eventSink(AddRoomToSpaceEvent.UpdateSearchQuery("nonexistent"))
advanceUntilIdle()
val finalState = expectMostRecentItem()
assertThat(finalState.isSearchActive).isTrue()
assertThat(finalState.searchQuery).isEqualTo("nonexistent")
assertThat(finalState.searchResults).isInstanceOf(SearchBarResultState.NoResultsFound::class.java)
}
}
@Test
fun `present - Save triggers addChildToSpace for all selected rooms`() = runTest {
val addChildToSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, _ ->
Result.success(Unit)
}
val spaceService = FakeSpaceService(
addChildToSpaceResult = addChildToSpaceResult,
)
val presenter = createPresenter(spaceService = spaceService)
presenter.test {
val state = awaitItem()
// Select two rooms
val room1 = aSelectRoomInfoList()[0]
val room2 = aSelectRoomInfoList()[1]
state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room1))
awaitItem()
state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room2))
awaitItem()
// Save
state.eventSink(AddRoomToSpaceEvent.Save)
// Wait for loading and success states
skipItems(1) // Loading
advanceUntilIdle()
skipItems(1) // Success
// Verify service was called for both rooms
addChildToSpaceResult.assertions().isCalledExactly(2)
}
}
@Test
fun `present - Save success updates saveAction to Success`() = runTest {
val spaceService = FakeSpaceService(
addChildToSpaceResult = { _, _ -> Result.success(Unit) },
)
val presenter = createPresenter(spaceService = spaceService)
presenter.test {
val state = awaitItem()
val room = aSelectRoomInfoList().first()
state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room))
awaitItem()
state.eventSink(AddRoomToSpaceEvent.Save)
// Wait for loading state
val loadingState = awaitItem()
assertThat(loadingState.saveAction).isEqualTo(AsyncAction.Loading)
// Wait for success state
advanceUntilIdle()
val successState = awaitItem()
assertThat(successState.saveAction).isInstanceOf(AsyncAction.Success::class.java)
}
}
@Test
fun `present - Save failure updates saveAction to Failure`() = runTest {
val spaceService = FakeSpaceService(
addChildToSpaceResult = { _, _ -> Result.failure(AN_EXCEPTION) },
)
val presenter = createPresenter(spaceService = spaceService)
presenter.test {
val state = awaitItem()
val room = aSelectRoomInfoList().first()
state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room))
awaitItem()
state.eventSink(AddRoomToSpaceEvent.Save)
// Wait for loading state
val loadingState = awaitItem()
assertThat(loadingState.saveAction).isEqualTo(AsyncAction.Loading)
// Wait for failure state
advanceUntilIdle()
val failureState = awaitItem()
assertThat(failureState.saveAction).isInstanceOf(AsyncAction.Failure::class.java)
}
}
@Test
fun `present - ResetSaveAction resets to Uninitialized`() = runTest {
val spaceService = FakeSpaceService(
addChildToSpaceResult = { _, _ -> Result.success(Unit) },
)
val presenter = createPresenter(spaceService = spaceService)
presenter.test {
val state = awaitItem()
val room = aSelectRoomInfoList().first()
state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room))
awaitItem()
state.eventSink(AddRoomToSpaceEvent.Save)
skipItems(1) // Loading
advanceUntilIdle()
val successState = awaitItem()
assertThat(successState.saveAction).isInstanceOf(AsyncAction.Success::class.java)
// Reset
successState.eventSink(AddRoomToSpaceEvent.ResetSaveAction)
val resetState = awaitItem()
assertThat(resetState.saveAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@Test
fun `canSave is false when no rooms selected`() = runTest {
val presenter = createPresenter()
presenter.test {
val state = awaitItem()
assertThat(state.selectedRooms).isEmpty()
assertThat(state.canSave).isFalse()
}
}
@Test
fun `canSave is false when loading`() = runTest {
val spaceService = FakeSpaceService(
addChildToSpaceResult = { _, _ -> Result.success(Unit) },
)
val presenter = createPresenter(spaceService = spaceService)
presenter.test {
val state = awaitItem()
val room = aSelectRoomInfoList().first()
state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room))
val stateWithRoom = awaitItem()
assertThat(stateWithRoom.canSave).isTrue()
stateWithRoom.eventSink(AddRoomToSpaceEvent.Save)
val loadingState = awaitItem()
assertThat(loadingState.saveAction).isEqualTo(AsyncAction.Loading)
assertThat(loadingState.canSave).isFalse()
}
}
private fun TestScope.createPresenter(
spaceRoomList: FakeSpaceRoomList = FakeSpaceRoomList(
paginateResult = { Result.success(Unit) },
),
spaceService: FakeSpaceService = FakeSpaceService(
addChildToSpaceResult = { _, _ -> Result.success(Unit) },
),
roomListService: FakeRoomListService = FakeRoomListService(),
matrixClient: FakeMatrixClient = FakeMatrixClient(
roomListService = roomListService,
),
): AddRoomToSpacePresenter {
val dataSourceFactory = object : AddRoomToSpaceSearchDataSource.Factory {
override fun create(coroutineScope: CoroutineScope) = AddRoomToSpaceSearchDataSource(
coroutineScope = coroutineScope,
roomListService = roomListService,
spaceRoomList = spaceRoomList,
matrixClient = matrixClient,
coroutineDispatchers = testCoroutineDispatchers(),
)
}
return AddRoomToSpacePresenter(
spaceRoomList = spaceRoomList,
spaceService = spaceService,
dataSourceFactory = dataSourceFactory,
)
}
}

View file

@ -0,0 +1,124 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 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.space.impl.addroom
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.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack
import kotlinx.collections.immutable.toImmutableList
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 AddRoomToSpaceViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking back when search inactive invokes onBackClick`() {
ensureCalledOnce {
rule.setAddRoomToSpaceView(
anAddRoomToSpaceState(
isSearchActive = false,
),
onBackClick = it,
)
rule.pressBack()
}
}
@Test
fun `clicking back when search active emits CloseSearch event`() {
val eventsRecorder = EventsRecorder<AddRoomToSpaceEvent>()
rule.setAddRoomToSpaceView(
anAddRoomToSpaceState(
isSearchActive = true,
eventSink = eventsRecorder,
),
)
rule.pressBack()
eventsRecorder.assertSingle(AddRoomToSpaceEvent.CloseSearch)
}
@Test
fun `clicking save emits Save event`() {
val eventsRecorder = EventsRecorder<AddRoomToSpaceEvent>()
rule.setAddRoomToSpaceView(
anAddRoomToSpaceState(
selectedRooms = aSelectRoomInfoList().take(1).toImmutableList(),
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_save)
eventsRecorder.assertList(
listOf(
AddRoomToSpaceEvent.UpdateSearchQuery(""), // SearchBar initialization
AddRoomToSpaceEvent.Save,
)
)
}
@Config(qualifiers = "h1024dp")
@Test
fun `clicking room in suggestions emits ToggleRoom event`() {
val suggestions = aSelectRoomInfoList()
val eventsRecorder = EventsRecorder<AddRoomToSpaceEvent>()
rule.setAddRoomToSpaceView(
anAddRoomToSpaceState(
suggestions = suggestions,
eventSink = eventsRecorder,
),
)
rule.onNodeWithText(suggestions.first().name!!).performClick()
eventsRecorder.assertList(
listOf(
AddRoomToSpaceEvent.UpdateSearchQuery(""), // SearchBar initialization
AddRoomToSpaceEvent.ToggleRoom(suggestions.first()),
)
)
}
@Test
fun `onRoomsAdded called when saveAction is Success`() {
ensureCalledOnce {
rule.setAddRoomToSpaceView(
anAddRoomToSpaceState(
saveAction = AsyncAction.Success(Unit),
),
onRoomsAdded = it,
)
}
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAddRoomToSpaceView(
state: AddRoomToSpaceState,
onBackClick: () -> Unit = EnsureNeverCalled(),
onRoomsAdded: () -> Unit = EnsureNeverCalled(),
) {
setContent {
AddRoomToSpaceView(
state = state,
onBackClick = onBackClick,
onRoomsAdded = onRoomsAdded,
)
}
}