Browse Source

Room directory : add tests and cleanup

pull/2620/head
ganfra 2 months ago
parent
commit
3f1f764745
  1. 2
      .idea/kotlinc.xml
  2. 2
      features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt
  3. 9
      features/roomdirectory/impl/build.gradle.kts
  4. 2
      features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt
  5. 1
      features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt
  6. 11
      features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt
  7. 63
      features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt
  8. 48
      features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt
  9. 32
      features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/di/JoinRoom.kt
  10. 1
      features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryListState.kt
  11. 26
      features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/FakeJoinRoom.kt
  12. 182
      features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenterTest.kt
  13. 112
      features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt
  14. 2
      features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt
  15. 22
      features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTests.kt
  16. 3
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
  17. 1
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt
  18. 1
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchProcessor.kt
  19. 1
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt
  20. 1
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt
  21. 5
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
  22. 31
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryList.kt
  23. 8
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt
  24. 41
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/RoomDescriptionFixture.kt
  25. 5
      libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt
  26. 4
      samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt
  27. 34
      tests/testutils/src/main/kotlin/io/element/android/tests/testutils/PresenterTest.kt

2
.idea/kotlinc.xml

@ -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>

2
features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt

@ -23,7 +23,6 @@ import io.element.android.libraries.architecture.FeatureEntryPoint @@ -23,7 +23,6 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.matrix.api.core.RoomId
interface RoomDirectoryEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
interface NodeBuilder {
@ -35,4 +34,3 @@ interface RoomDirectoryEntryPoint : FeatureEntryPoint { @@ -35,4 +34,3 @@ interface RoomDirectoryEntryPoint : FeatureEntryPoint {
fun onOpenRoom(roomId: RoomId)
}
}

9
features/roomdirectory/impl/build.gradle.kts

@ -25,6 +25,11 @@ plugins { @@ -25,6 +25,11 @@ plugins {
android {
namespace = "io.element.android.features.roomdirectory.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
anvil {
@ -41,13 +46,17 @@ dependencies { @@ -41,13 +46,17 @@ dependencies {
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.testtags)
testImplementation(libs.test.junit)
testImplementation(libs.androidx.compose.ui.test.junit)
testImplementation(libs.test.robolectric)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.tests.testutils)
ksp(libs.showkase.processor)
}

2
features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt

@ -28,12 +28,10 @@ import javax.inject.Inject @@ -28,12 +28,10 @@ import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultRoomDirectoryEntryPoint @Inject constructor() : RoomDirectoryEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomDirectoryEntryPoint.NodeBuilder {
val plugins = ArrayList<Plugin>()
return object : RoomDirectoryEntryPoint.NodeBuilder {
override fun callback(callback: RoomDirectoryEntryPoint.Callback): RoomDirectoryEntryPoint.NodeBuilder {
plugins += callback
return this

1
features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt

@ -35,7 +35,6 @@ class RoomDirectoryNode @AssistedInject constructor( @@ -35,7 +35,6 @@ class RoomDirectoryNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>,
private val presenter: RoomDirectoryPresenter,
) : Node(buildContext, plugins = plugins) {
private fun onRoomJoined(roomId: RoomId) {
plugins<RoomDirectoryEntryPoint.Callback>().forEach {
it.onOpenRoom(roomId)

11
features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt

@ -26,13 +26,13 @@ import androidx.compose.runtime.remember @@ -26,13 +26,13 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import io.element.android.features.roomdirectory.impl.root.di.JoinRoom
import io.element.android.features.roomdirectory.impl.root.model.RoomDirectoryListState
import io.element.android.features.roomdirectory.impl.root.model.toFeatureModel
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.MatrixClient
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
@ -46,10 +46,9 @@ import javax.inject.Inject @@ -46,10 +46,9 @@ import javax.inject.Inject
class RoomDirectoryPresenter @Inject constructor(
private val dispatchers: CoroutineDispatchers,
private val matrixClient: MatrixClient,
private val joinRoom: JoinRoom,
private val roomDirectoryService: RoomDirectoryService,
) : Presenter<RoomDirectoryState> {
@Composable
override fun present(): RoomDirectoryState {
var loadingMore by remember {
@ -68,9 +67,9 @@ class RoomDirectoryPresenter @Inject constructor( @@ -68,9 +67,9 @@ class RoomDirectoryPresenter @Inject constructor(
}
LaunchedEffect(searchQuery) {
if (searchQuery == null) return@LaunchedEffect
//debounce search query
// debounce search query
delay(300)
//cancel load more right away
// cancel load more right away
loadingMore = false
roomDirectoryList.filter(searchQuery, 20)
}
@ -108,7 +107,7 @@ class RoomDirectoryPresenter @Inject constructor( @@ -108,7 +107,7 @@ class RoomDirectoryPresenter @Inject constructor(
private fun CoroutineScope.joinRoom(state: MutableState<AsyncAction<RoomId>>, roomId: RoomId) = launch {
state.runUpdatingState {
matrixClient.joinRoom(roomId)
joinRoom(roomId)
}
}

63
features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt

@ -25,39 +25,14 @@ import io.element.android.libraries.matrix.api.core.RoomId @@ -25,39 +25,14 @@ import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
open class RoomDirectorySearchStateProvider : PreviewParameterProvider<RoomDirectoryState> {
open class RoomDirectoryStateProvider : PreviewParameterProvider<RoomDirectoryState> {
override val values: Sequence<RoomDirectoryState>
get() = sequenceOf(
aRoomDirectoryState(),
aRoomDirectoryState(
query = "Element",
roomDescriptions = persistentListOf(
RoomDescription(
roomId = RoomId("@exa:matrix.org"),
name = "Element X Android",
description = "Element X is a secure, private and decentralized messenger.",
avatarData = AvatarData(
id = "@exa:matrix.org",
name = "Element X Android",
url = null,
size = AvatarSize.RoomDirectoryItem
),
canBeJoined = true,
),
RoomDescription(
roomId = RoomId("@exi:matrix.org"),
name = "Element X iOS",
description = "Element X is a secure, private and decentralized messenger.",
avatarData = AvatarData(
id = "@exi:matrix.org",
name = "Element X iOS",
url = null,
size = AvatarSize.RoomDirectoryItem
),
canBeJoined = false,
)
)
),
roomDescriptions = aRoomDescriptionList(),
)
)
}
@ -66,10 +41,40 @@ fun aRoomDirectoryState( @@ -66,10 +41,40 @@ fun aRoomDirectoryState(
displayLoadMoreIndicator: Boolean = false,
roomDescriptions: ImmutableList<RoomDescription> = persistentListOf(),
joinRoomAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
eventSink: (RoomDirectoryEvents) -> Unit = {},
) = RoomDirectoryState(
query = query,
roomDescriptions = roomDescriptions,
displayLoadMoreIndicator = displayLoadMoreIndicator,
joinRoomAction = joinRoomAction,
eventSink = {},
eventSink = eventSink,
)
fun aRoomDescriptionList(): ImmutableList<RoomDescription> {
return persistentListOf(
RoomDescription(
roomId = RoomId("!exa:matrix.org"),
name = "Element X Android",
description = "Element X is a secure, private and decentralized messenger.",
avatarData = AvatarData(
id = "!exa:matrix.org",
name = "Element X Android",
url = null,
size = AvatarSize.RoomDirectoryItem
),
canBeJoined = true,
),
RoomDescription(
roomId = RoomId("!exi:matrix.org"),
name = "Element X iOS",
description = "Element X is a secure, private and decentralized messenger.",
avatarData = AvatarData(
id = "!exi:matrix.org",
name = "Element X iOS",
url = null,
size = AvatarSize.RoomDirectoryItem
),
canBeJoined = false,
)
)
}

48
features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt

@ -40,6 +40,7 @@ import androidx.compose.ui.Alignment @@ -40,6 +40,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -61,6 +62,7 @@ import io.element.android.libraries.designsystem.theme.components.Text @@ -61,6 +62,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
@ -71,7 +73,6 @@ fun RoomDirectoryView( @@ -71,7 +73,6 @@ fun RoomDirectoryView(
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
fun joinRoom(roomId: RoomId) {
state.eventSink(RoomDirectoryEvents.JoinRoom(roomId))
}
@ -86,8 +87,8 @@ fun RoomDirectoryView( @@ -86,8 +87,8 @@ fun RoomDirectoryView(
state = state,
onResultClicked = ::joinRoom,
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)
.padding(padding)
.consumeWindowInsets(padding)
)
}
)
@ -96,7 +97,8 @@ fun RoomDirectoryView( @@ -96,7 +97,8 @@ fun RoomDirectoryView(
onSuccess = onRoomJoined,
onErrorDismiss = {
state.eventSink(RoomDirectoryEvents.JoinRoomDismissError)
})
}
)
}
@OptIn(ExperimentalMaterial3Api::class)
@ -171,7 +173,7 @@ private fun RoomDirectoryRoomList( @@ -171,7 +173,7 @@ private fun RoomDirectoryRoomList(
if (displayLoadMoreIndicator) {
item {
LoadMoreIndicator(modifier = Modifier.fillMaxWidth())
LaunchedEffect(Unit) {
LaunchedEffect(onReachedLoadMore) {
onReachedLoadMore()
}
}
@ -182,10 +184,10 @@ private fun RoomDirectoryRoomList( @@ -182,10 +184,10 @@ private fun RoomDirectoryRoomList(
@Composable
private fun LoadMoreIndicator(modifier: Modifier = Modifier) {
Box(
modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(24.dp),
modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(24.dp),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
@ -213,7 +215,7 @@ private fun SearchTextField( @@ -213,7 +215,7 @@ private fun SearchTextField(
) {
val focusManager = LocalFocusManager.current
TextField(
modifier = modifier,
modifier = modifier.testTag(TestTags.searchTextField.value),
textStyle = ElementTheme.typography.fontBodyLgRegular,
singleLine = true,
value = query,
@ -255,14 +257,14 @@ private fun RoomDirectoryRoomRow( @@ -255,14 +257,14 @@ private fun RoomDirectoryRoomRow(
) {
Row(
modifier = modifier
.fillMaxWidth()
.clickable { onClick(roomDescription.roomId) }
.padding(
top = 12.dp,
bottom = 12.dp,
start = 16.dp,
)
.height(IntrinsicSize.Min),
.fillMaxWidth()
.clickable { onClick(roomDescription.roomId) }
.padding(
top = 12.dp,
bottom = 12.dp,
start = 16.dp,
)
.height(IntrinsicSize.Min),
) {
Avatar(
avatarData = roomDescription.avatarData,
@ -270,8 +272,8 @@ private fun RoomDirectoryRoomRow( @@ -270,8 +272,8 @@ private fun RoomDirectoryRoomRow(
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp)
.weight(1f)
.padding(start = 16.dp)
) {
Text(
text = roomDescription.name,
@ -293,8 +295,8 @@ private fun RoomDirectoryRoomRow( @@ -293,8 +295,8 @@ private fun RoomDirectoryRoomRow(
text = stringResource(id = CommonStrings.action_join),
color = ElementTheme.colors.textSuccessPrimary,
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 4.dp, end = 12.dp)
.align(Alignment.CenterVertically)
.padding(start = 4.dp, end = 12.dp)
)
} else {
Spacer(modifier = Modifier.width(24.dp))
@ -304,7 +306,7 @@ private fun RoomDirectoryRoomRow( @@ -304,7 +306,7 @@ private fun RoomDirectoryRoomRow(
@PreviewsDayNight
@Composable
fun RoomDirectorySearchViewLightPreview(@PreviewParameter(RoomDirectorySearchStateProvider::class) state: RoomDirectoryState) = ElementPreview {
internal fun RoomDirectoryViewPreview(@PreviewParameter(RoomDirectoryStateProvider::class) state: RoomDirectoryState) = ElementPreview {
RoomDirectoryView(
state = state,
onRoomJoined = {},

32
features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/di/JoinRoom.kt

@ -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)
}

1
features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryListState.kt

@ -24,7 +24,6 @@ internal data class RoomDirectoryListState( @@ -24,7 +24,6 @@ internal data class RoomDirectoryListState(
val hasMoreToLoad: Boolean,
val items: ImmutableList<RoomDescription>,
) {
companion object {
val Default = RoomDirectoryListState(
hasMoreToLoad = true,

26
features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/FakeJoinRoom.kt

@ -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)
}

182
features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenterTest.kt

@ -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,
)
}
}

112
features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt

@ -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,
)
}
}

2
features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt

@ -191,6 +191,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL @@ -191,6 +191,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
onInvitesClicked: () -> Unit = EnsureNeverCalled(),
onRoomSettingsClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
onMenuActionClicked: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(),
onRoomDirectorySearchClicked: () -> Unit = EnsureNeverCalled(),
) {
setContent {
RoomListView(
@ -203,6 +204,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL @@ -203,6 +204,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
onInvitesClicked = onInvitesClicked,
onRoomSettingsClicked = onRoomSettingsClicked,
onMenuActionClicked = onMenuActionClicked,
onRoomDirectorySearchClicked = onRoomDirectorySearchClicked,
)
}
}

22
features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTests.kt

@ -23,6 +23,9 @@ import com.google.common.truth.Truth.assertThat @@ -23,6 +23,9 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
@ -128,10 +131,26 @@ class RoomListSearchPresenterTests { @@ -128,10 +131,26 @@ class RoomListSearchPresenterTests {
}
}
}
@Test
fun `present - room directory search`() = runTest {
val featureFlagService = FakeFeatureFlagService()
featureFlagService.setFeatureEnabled(FeatureFlags.RoomDirectorySearch, true)
val presenter = createRoomListSearchPresenter(featureFlagService = featureFlagService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
awaitItem().let { state ->
assertThat(state.isRoomDirectorySearchEnabled).isTrue()
}
}
}
}
fun TestScope.createRoomListSearchPresenter(
roomListService: RoomListService = FakeRoomListService(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
): RoomListSearchPresenter {
return RoomListSearchPresenter(
dataSource = RoomListSearchDataSource(
@ -141,6 +160,7 @@ fun TestScope.createRoomListSearchPresenter( @@ -141,6 +160,7 @@ fun TestScope.createRoomListSearchPresenter(
roomLastMessageFormatter = FakeRoomLastMessageFormatter(),
),
coroutineDispatchers = testCoroutineDispatchers(),
)
),
featureFlagService = featureFlagService,
)
}

3
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt

@ -76,7 +76,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -76,7 +76,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@ -441,7 +440,7 @@ class RustMatrixClient( @@ -441,7 +440,7 @@ class RustMatrixClient(
runCatching { client.removeAvatar() }
}
override suspend fun joinRoom(roomId: RoomId): Result<RoomId> = withContext(sessionDispatcher) {
override suspend fun joinRoom(roomId: RoomId): Result<RoomId> = withContext(sessionDispatcher) {
runCatching {
client.joinRoomById(roomId.value).destroy()
try {

1
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt

@ -22,7 +22,6 @@ import org.matrix.rustcomponents.sdk.PublicRoomJoinRule @@ -22,7 +22,6 @@ import org.matrix.rustcomponents.sdk.PublicRoomJoinRule
import org.matrix.rustcomponents.sdk.RoomDescription as RustRoomDescription
class RoomDescriptionMapper {
fun map(roomDescription: RustRoomDescription): RoomDescription {
return RoomDescription(
roomId = RoomId(roomDescription.roomId),

1
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchProcessor.kt

@ -30,7 +30,6 @@ class RoomDirectorySearchProcessor( @@ -30,7 +30,6 @@ class RoomDirectorySearchProcessor(
private val coroutineContext: CoroutineContext,
private val roomDescriptionMapper: RoomDescriptionMapper,
) {
private val mutex = Mutex()
suspend fun postUpdates(updates: List<RoomDirectorySearchEntryUpdate>) {

1
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt

@ -35,7 +35,6 @@ class RustRoomDirectoryList( @@ -35,7 +35,6 @@ class RustRoomDirectoryList(
coroutineScope: CoroutineScope,
private val coroutineContext: CoroutineContext,
) : RoomDirectoryList {
private val hasMoreToLoad = MutableStateFlow(true)
private val items = MutableSharedFlow<List<RoomDescription>>(replay = 1)
private val processor = RoomDirectorySearchProcessor(items, coroutineContext, RoomDescriptionMapper())

1
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt

@ -26,7 +26,6 @@ class RustRoomDirectoryService( @@ -26,7 +26,6 @@ class RustRoomDirectoryService(
private val client: Client,
private val sessionDispatcher: CoroutineDispatcher,
) : RoomDirectoryService {
override fun createRoomDirectoryList(scope: CoroutineScope): RoomDirectoryList {
return RustRoomDirectoryList(client.roomDirectorySearch(), scope, sessionDispatcher)
}

5
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt

@ -94,6 +94,9 @@ class FakeMatrixClient( @@ -94,6 +94,9 @@ class FakeMatrixClient(
private var setDisplayNameResult: Result<Unit> = Result.success(Unit)
private var uploadAvatarResult: Result<Unit> = Result.success(Unit)
private var removeAvatarResult: Result<Unit> = Result.success(Unit)
var joinRoomLambda: suspend (RoomId) -> Result<RoomId> = {
Result.success(it)
}
override suspend fun getRoom(roomId: RoomId): MatrixRoom? {
return getRoomResults[roomId]
@ -181,6 +184,8 @@ class FakeMatrixClient( @@ -181,6 +184,8 @@ class FakeMatrixClient(
return removeAvatarResult
}
override suspend fun joinRoom(roomId: RoomId): Result<RoomId> = joinRoomLambda(roomId)
override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService
override fun pushersService(): PushersService = pushersService

31
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryList.kt

@ -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()
}

8
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt

@ -20,8 +20,8 @@ import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList @@ -20,8 +20,8 @@ import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import kotlinx.coroutines.CoroutineScope
class FakeRoomDirectoryService : RoomDirectoryService {
override fun createRoomDirectoryList(scope: CoroutineScope): RoomDirectoryList {
TODO("Not yet implemented")
}
class FakeRoomDirectoryService(
private val createRoomDirectoryListFactory: (CoroutineScope) -> RoomDirectoryList = { throw AssertionError("Configure a proper factory.") }
) : RoomDirectoryService {
override fun createRoomDirectoryList(scope: CoroutineScope) = createRoomDirectoryListFactory(scope)
}

41
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/RoomDescriptionFixture.kt

@ -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
)

5
libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt

@ -100,4 +100,9 @@ object TestTags { @@ -100,4 +100,9 @@ object TestTags {
* Timeline item.
*/
val timelineItemSenderInfo = TestTag("timeline_item-sender_info")
/**
* Search field.
*/
val searchTextField = TestTag("search_text_field")
}

4
samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt

@ -118,7 +118,8 @@ class RoomListScreen( @@ -118,7 +118,8 @@ class RoomListScreen(
roomListService = matrixClient.roomListService,
roomSummaryFactory = roomListRoomSummaryFactory,
coroutineDispatchers = coroutineDispatchers,
)
),
featureFlagService = featureFlagService,
),
sessionPreferencesStore = DefaultSessionPreferencesStore(
context = context,
@ -156,6 +157,7 @@ class RoomListScreen( @@ -156,6 +157,7 @@ class RoomListScreen(
onInvitesClicked = {},
onRoomSettingsClicked = {},
onMenuActionClicked = {},
onRoomDirectorySearchClicked = {},
modifier = modifier,
)

34
tests/testutils/src/main/kotlin/io/element/android/tests/testutils/PresenterTest.kt

@ -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…
Cancel
Save