Add tests to AddRoomToSpace feature
This commit is contained in:
parent
d45d1e0327
commit
df62694b2f
3 changed files with 464 additions and 3 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue