ganfra
2 months ago
27 changed files with 573 additions and 77 deletions
@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project version="4"> |
||||
<component name="KotlinJpsPluginSettings"> |
||||
<option name="version" value="1.9.22" /> |
||||
<option name="version" value="1.9.23" /> |
||||
</component> |
||||
</project> |
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.roomdirectory.impl.root.di |
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding |
||||
import io.element.android.libraries.di.SessionScope |
||||
import io.element.android.libraries.matrix.api.MatrixClient |
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
import javax.inject.Inject |
||||
|
||||
interface JoinRoom { |
||||
suspend operator fun invoke(roomId: RoomId): Result<RoomId> |
||||
} |
||||
|
||||
@ContributesBinding(SessionScope::class) |
||||
class DefaultJoinRoom @Inject constructor(private val client: MatrixClient) : JoinRoom { |
||||
override suspend fun invoke(roomId: RoomId) = client.joinRoom(roomId) |
||||
} |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.roomdirectory.impl.root |
||||
|
||||
import io.element.android.features.roomdirectory.impl.root.di.JoinRoom |
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
|
||||
class FakeJoinRoom( |
||||
var lambda: (RoomId) -> Result<RoomId> = { Result.success(it) } |
||||
) : JoinRoom { |
||||
override suspend fun invoke(roomId: RoomId) = lambda(roomId) |
||||
} |
@ -0,0 +1,182 @@
@@ -0,0 +1,182 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.roomdirectory.impl.root |
||||
|
||||
import com.google.common.truth.Truth.assertThat |
||||
import io.element.android.features.roomdirectory.impl.root.di.JoinRoom |
||||
import io.element.android.libraries.architecture.AsyncAction |
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList |
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService |
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID |
||||
import io.element.android.libraries.matrix.test.roomdirectory.FakeRoomDirectoryList |
||||
import io.element.android.libraries.matrix.test.roomdirectory.FakeRoomDirectoryService |
||||
import io.element.android.libraries.matrix.test.roomdirectory.aRoomDescription |
||||
import io.element.android.tests.testutils.lambda.any |
||||
import io.element.android.tests.testutils.lambda.assert |
||||
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.ExperimentalCoroutinesApi |
||||
import kotlinx.coroutines.flow.MutableSharedFlow |
||||
import kotlinx.coroutines.test.TestScope |
||||
import kotlinx.coroutines.test.advanceUntilIdle |
||||
import kotlinx.coroutines.test.runTest |
||||
import org.junit.Test |
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class) class RoomDirectoryPresenterTest { |
||||
@Test |
||||
fun `present - initial state`() = runTest { |
||||
val presenter = createRoomDirectoryPresenter() |
||||
presenter.test { |
||||
val initialState = awaitItem() |
||||
assertThat(initialState.query).isEmpty() |
||||
assertThat(initialState.displayEmptyState).isFalse() |
||||
assertThat(initialState.joinRoomAction).isEqualTo(AsyncAction.Uninitialized) |
||||
assertThat(initialState.roomDescriptions).isEmpty() |
||||
assertThat(initialState.displayLoadMoreIndicator).isTrue() |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - room directory list emits empty state`() = runTest { |
||||
val directoryListStateFlow = MutableSharedFlow<RoomDirectoryList.State>(replay = 1) |
||||
val roomDirectoryList = FakeRoomDirectoryList(directoryListStateFlow) |
||||
val roomDirectoryService = FakeRoomDirectoryService { roomDirectoryList } |
||||
val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService) |
||||
presenter.test { |
||||
skipItems(1) |
||||
directoryListStateFlow.emit( |
||||
RoomDirectoryList.State(false, emptyList()) |
||||
) |
||||
awaitItem().also { state -> |
||||
assertThat(state.displayEmptyState).isTrue() |
||||
} |
||||
cancelAndIgnoreRemainingEvents() |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - room directory list emits non-empty state`() = runTest { |
||||
val directoryListStateFlow = MutableSharedFlow<RoomDirectoryList.State>(replay = 1) |
||||
val roomDirectoryList = FakeRoomDirectoryList(directoryListStateFlow) |
||||
val roomDirectoryService = FakeRoomDirectoryService { roomDirectoryList } |
||||
val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService) |
||||
presenter.test { |
||||
skipItems(1) |
||||
directoryListStateFlow.emit( |
||||
RoomDirectoryList.State( |
||||
hasMoreToLoad = true, |
||||
items = listOf(aRoomDescription()) |
||||
) |
||||
) |
||||
awaitItem().also { state -> |
||||
assertThat(state.displayEmptyState).isFalse() |
||||
assertThat(state.roomDescriptions).hasSize(1) |
||||
} |
||||
cancelAndIgnoreRemainingEvents() |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - emit search event`() = runTest { |
||||
val filterLambda = lambdaRecorder { _: String?, _: Int -> |
||||
Result.success(Unit) |
||||
} |
||||
val roomDirectoryList = FakeRoomDirectoryList(filterLambda = filterLambda) |
||||
val roomDirectoryService = FakeRoomDirectoryService { roomDirectoryList } |
||||
val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService) |
||||
presenter.test { |
||||
awaitItem().also { state -> |
||||
state.eventSink(RoomDirectoryEvents.Search("test")) |
||||
} |
||||
awaitItem().also { state -> |
||||
assertThat(state.query).isEqualTo("test") |
||||
} |
||||
advanceUntilIdle() |
||||
cancelAndIgnoreRemainingEvents() |
||||
} |
||||
assert(filterLambda) |
||||
.isCalledOnce() |
||||
.with(value("test"), any()) |
||||
} |
||||
|
||||
@Test |
||||
fun `present - emit load more event`() = runTest { |
||||
val loadMoreLambda = lambdaRecorder { -> |
||||
Result.success(Unit) |
||||
} |
||||
val roomDirectoryList = FakeRoomDirectoryList(loadMoreLambda = loadMoreLambda) |
||||
val roomDirectoryService = FakeRoomDirectoryService { roomDirectoryList } |
||||
val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService) |
||||
presenter.test { |
||||
awaitItem().also { state -> |
||||
state.eventSink(RoomDirectoryEvents.LoadMore) |
||||
} |
||||
advanceUntilIdle() |
||||
cancelAndIgnoreRemainingEvents() |
||||
} |
||||
assert(loadMoreLambda) |
||||
.isCalledOnce() |
||||
.withNoParameter() |
||||
} |
||||
|
||||
@Test |
||||
fun `present - emit join room event`() = runTest { |
||||
val joinRoomSuccess = lambdaRecorder { roomId: RoomId -> |
||||
Result.success(roomId) |
||||
} |
||||
val joinRoomFailure = lambdaRecorder { roomId: RoomId -> |
||||
Result.failure<RoomId>(RuntimeException("Failed to join room $roomId")) |
||||
} |
||||
val fakeJoinRoom = FakeJoinRoom(joinRoomSuccess) |
||||
val presenter = createRoomDirectoryPresenter(joinRoom = fakeJoinRoom) |
||||
presenter.test { |
||||
awaitItem().also { state -> |
||||
state.eventSink(RoomDirectoryEvents.JoinRoom(A_ROOM_ID)) |
||||
} |
||||
awaitItem().also { state -> |
||||
assertThat(state.joinRoomAction).isEqualTo(AsyncAction.Success(A_ROOM_ID)) |
||||
fakeJoinRoom.lambda = joinRoomFailure |
||||
state.eventSink(RoomDirectoryEvents.JoinRoom(A_ROOM_ID)) |
||||
} |
||||
awaitItem().also { state -> |
||||
assertThat(state.joinRoomAction).isInstanceOf(AsyncAction.Failure::class.java) |
||||
} |
||||
} |
||||
assert(joinRoomSuccess) |
||||
.isCalledOnce() |
||||
.with(value(A_ROOM_ID)) |
||||
assert(joinRoomFailure) |
||||
.isCalledOnce() |
||||
.with(value(A_ROOM_ID)) |
||||
} |
||||
|
||||
private fun TestScope.createRoomDirectoryPresenter( |
||||
roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService( |
||||
createRoomDirectoryListFactory = { FakeRoomDirectoryList() } |
||||
), |
||||
joinRoom: JoinRoom = FakeJoinRoom { Result.success(it) }, |
||||
): RoomDirectoryPresenter { |
||||
return RoomDirectoryPresenter( |
||||
dispatchers = testCoroutineDispatchers(), |
||||
joinRoom = joinRoom, |
||||
roomDirectoryService = roomDirectoryService, |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,112 @@
@@ -0,0 +1,112 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.roomdirectory.impl.root |
||||
|
||||
import androidx.activity.ComponentActivity |
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule |
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule |
||||
import androidx.compose.ui.test.onNodeWithTag |
||||
import androidx.compose.ui.test.onNodeWithText |
||||
import androidx.compose.ui.test.performClick |
||||
import androidx.compose.ui.test.performTextInput |
||||
import androidx.test.ext.junit.runners.AndroidJUnit4 |
||||
import io.element.android.libraries.architecture.AsyncAction |
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
import io.element.android.libraries.testtags.TestTags |
||||
import io.element.android.tests.testutils.EnsureNeverCalled |
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam |
||||
import io.element.android.tests.testutils.EventsRecorder |
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam |
||||
import org.junit.Rule |
||||
import org.junit.Test |
||||
import org.junit.rules.TestRule |
||||
import org.junit.runner.RunWith |
||||
|
||||
@RunWith(AndroidJUnit4::class) |
||||
class RoomDirectoryViewTest { |
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>() |
||||
|
||||
@Test |
||||
fun `typing text in search field emits the expected Event`() { |
||||
val eventsRecorder = EventsRecorder<RoomDirectoryEvents>() |
||||
rule.setRoomDirectoryView( |
||||
state = aRoomDirectoryState( |
||||
eventSink = eventsRecorder, |
||||
) |
||||
) |
||||
rule.onNodeWithTag(TestTags.searchTextField.value).performTextInput( |
||||
text = "Test" |
||||
) |
||||
eventsRecorder.assertSingle(RoomDirectoryEvents.Search("Test")) |
||||
} |
||||
|
||||
@Test |
||||
fun `clicking on room item emits the expected Event`() { |
||||
val eventsRecorder = EventsRecorder<RoomDirectoryEvents>() |
||||
val state = aRoomDirectoryState( |
||||
roomDescriptions = aRoomDescriptionList(), |
||||
eventSink = eventsRecorder, |
||||
) |
||||
rule.setRoomDirectoryView(state = state) |
||||
val clickedRoom = state.roomDescriptions.first() |
||||
rule.onNodeWithText(clickedRoom.name).performClick() |
||||
eventsRecorder.assertSingle(RoomDirectoryEvents.JoinRoom(clickedRoom.roomId)) |
||||
} |
||||
|
||||
@Test |
||||
fun `composing load more indicator emits expected Event`() { |
||||
val eventsRecorder = EventsRecorder<RoomDirectoryEvents>() |
||||
val state = aRoomDirectoryState( |
||||
displayLoadMoreIndicator = true, |
||||
eventSink = eventsRecorder, |
||||
) |
||||
rule.setRoomDirectoryView(state = state) |
||||
eventsRecorder.assertSingle(RoomDirectoryEvents.LoadMore) |
||||
} |
||||
|
||||
@Test |
||||
fun `when joining room with success then onRoomJoined lambda is called once`() { |
||||
val eventsRecorder = EventsRecorder<RoomDirectoryEvents>(expectEvents = false) |
||||
val roomDescriptions = aRoomDescriptionList() |
||||
val joinedRoomId = roomDescriptions.first().roomId |
||||
val state = aRoomDirectoryState( |
||||
joinRoomAction = AsyncAction.Success(joinedRoomId), |
||||
roomDescriptions = roomDescriptions, |
||||
eventSink = eventsRecorder, |
||||
) |
||||
ensureCalledOnceWithParam(joinedRoomId) { callback -> |
||||
rule.setRoomDirectoryView( |
||||
state = state, |
||||
onRoomJoined = callback, |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomDirectoryView( |
||||
state: RoomDirectoryState, |
||||
onBackPressed: () -> Unit = EnsureNeverCalled(), |
||||
onRoomJoined: (RoomId) -> Unit = EnsureNeverCalledWithParam(), |
||||
) { |
||||
setContent { |
||||
RoomDirectoryView( |
||||
state = state, |
||||
onRoomJoined = onRoomJoined, |
||||
onBackPressed = onBackPressed, |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.matrix.test.roomdirectory |
||||
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList |
||||
import kotlinx.coroutines.flow.Flow |
||||
import kotlinx.coroutines.flow.emptyFlow |
||||
|
||||
class FakeRoomDirectoryList( |
||||
override val state: Flow<RoomDirectoryList.State> = emptyFlow(), |
||||
val filterLambda: (String?, Int) -> Result<Unit> = { _, _ -> Result.success(Unit) }, |
||||
val loadMoreLambda: () -> Result<Unit> = { Result.success(Unit) } |
||||
) : RoomDirectoryList { |
||||
override suspend fun filter(filter: String?, batchSize: Int) = filterLambda(filter, batchSize) |
||||
|
||||
override suspend fun loadMore(): Result<Unit> = loadMoreLambda() |
||||
} |
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.matrix.test.roomdirectory |
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription |
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID |
||||
|
||||
fun aRoomDescription( |
||||
roomId: RoomId = A_ROOM_ID, |
||||
name: String? = null, |
||||
topic: String? = null, |
||||
alias: String? = null, |
||||
avatarUrl: String? = null, |
||||
joinRule: RoomDescription.JoinRule = RoomDescription.JoinRule.UNKNOWN, |
||||
isWorldReadable: Boolean = true, |
||||
joinedMembers: Long = 2L |
||||
) = RoomDescription( |
||||
roomId = roomId, |
||||
name = name, |
||||
topic = topic, |
||||
alias = alias, |
||||
avatarUrl = avatarUrl, |
||||
joinRule = joinRule, |
||||
isWorldReadable = isWorldReadable, |
||||
joinedMembers = joinedMembers |
||||
) |
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.tests.testutils |
||||
|
||||
import app.cash.molecule.RecompositionMode |
||||
import app.cash.molecule.moleculeFlow |
||||
import app.cash.turbine.TurbineTestContext |
||||
import app.cash.turbine.test |
||||
import io.element.android.libraries.architecture.Presenter |
||||
import kotlin.time.Duration |
||||
|
||||
suspend fun <State> Presenter<State>.test( |
||||
timeout: Duration? = null, |
||||
name: String? = null, |
||||
validate: suspend TurbineTestContext<State>.() -> Unit, |
||||
) { |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
present() |
||||
}.test(timeout, name, validate) |
||||
} |
Loading…
Reference in new issue