Room directory : add tests and cleanup
This commit is contained in:
parent
37d645d153
commit
3f1f764745
27 changed files with 573 additions and 77 deletions
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="1.9.22" />
|
<option name="version" value="1.9.23" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -23,7 +23,6 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
interface RoomDirectoryEntryPoint : FeatureEntryPoint {
|
interface RoomDirectoryEntryPoint : FeatureEntryPoint {
|
||||||
|
|
||||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||||
|
|
||||||
interface NodeBuilder {
|
interface NodeBuilder {
|
||||||
|
|
@ -35,4 +34,3 @@ interface RoomDirectoryEntryPoint : FeatureEntryPoint {
|
||||||
fun onOpenRoom(roomId: RoomId)
|
fun onOpenRoom(roomId: RoomId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,11 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.roomdirectory.impl"
|
namespace = "io.element.android.features.roomdirectory.impl"
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
anvil {
|
anvil {
|
||||||
|
|
@ -41,13 +46,17 @@ dependencies {
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
|
implementation(projects.libraries.testtags)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
|
testImplementation(libs.test.robolectric)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
testImplementation(libs.molecule.runtime)
|
testImplementation(libs.molecule.runtime)
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,10 @@ import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultRoomDirectoryEntryPoint @Inject constructor() : RoomDirectoryEntryPoint {
|
class DefaultRoomDirectoryEntryPoint @Inject constructor() : RoomDirectoryEntryPoint {
|
||||||
|
|
||||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomDirectoryEntryPoint.NodeBuilder {
|
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomDirectoryEntryPoint.NodeBuilder {
|
||||||
val plugins = ArrayList<Plugin>()
|
val plugins = ArrayList<Plugin>()
|
||||||
|
|
||||||
return object : RoomDirectoryEntryPoint.NodeBuilder {
|
return object : RoomDirectoryEntryPoint.NodeBuilder {
|
||||||
|
|
||||||
override fun callback(callback: RoomDirectoryEntryPoint.Callback): RoomDirectoryEntryPoint.NodeBuilder {
|
override fun callback(callback: RoomDirectoryEntryPoint.Callback): RoomDirectoryEntryPoint.NodeBuilder {
|
||||||
plugins += callback
|
plugins += callback
|
||||||
return this
|
return this
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ class RoomDirectoryNode @AssistedInject constructor(
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val presenter: RoomDirectoryPresenter,
|
private val presenter: RoomDirectoryPresenter,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
private fun onRoomJoined(roomId: RoomId) {
|
private fun onRoomJoined(roomId: RoomId) {
|
||||||
plugins<RoomDirectoryEntryPoint.Callback>().forEach {
|
plugins<RoomDirectoryEntryPoint.Callback>().forEach {
|
||||||
it.onOpenRoom(roomId)
|
it.onOpenRoom(roomId)
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,13 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
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.RoomDirectoryListState
|
||||||
import io.element.android.features.roomdirectory.impl.root.model.toFeatureModel
|
import io.element.android.features.roomdirectory.impl.root.model.toFeatureModel
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runUpdatingState
|
import io.element.android.libraries.architecture.runUpdatingState
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
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.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
|
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
|
||||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
||||||
|
|
@ -46,10 +46,9 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class RoomDirectoryPresenter @Inject constructor(
|
class RoomDirectoryPresenter @Inject constructor(
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
private val matrixClient: MatrixClient,
|
private val joinRoom: JoinRoom,
|
||||||
private val roomDirectoryService: RoomDirectoryService,
|
private val roomDirectoryService: RoomDirectoryService,
|
||||||
) : Presenter<RoomDirectoryState> {
|
) : Presenter<RoomDirectoryState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): RoomDirectoryState {
|
override fun present(): RoomDirectoryState {
|
||||||
var loadingMore by remember {
|
var loadingMore by remember {
|
||||||
|
|
@ -68,9 +67,9 @@ class RoomDirectoryPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
LaunchedEffect(searchQuery) {
|
LaunchedEffect(searchQuery) {
|
||||||
if (searchQuery == null) return@LaunchedEffect
|
if (searchQuery == null) return@LaunchedEffect
|
||||||
//debounce search query
|
// debounce search query
|
||||||
delay(300)
|
delay(300)
|
||||||
//cancel load more right away
|
// cancel load more right away
|
||||||
loadingMore = false
|
loadingMore = false
|
||||||
roomDirectoryList.filter(searchQuery, 20)
|
roomDirectoryList.filter(searchQuery, 20)
|
||||||
}
|
}
|
||||||
|
|
@ -108,7 +107,7 @@ class RoomDirectoryPresenter @Inject constructor(
|
||||||
|
|
||||||
private fun CoroutineScope.joinRoom(state: MutableState<AsyncAction<RoomId>>, roomId: RoomId) = launch {
|
private fun CoroutineScope.joinRoom(state: MutableState<AsyncAction<RoomId>>, roomId: RoomId) = launch {
|
||||||
state.runUpdatingState {
|
state.runUpdatingState {
|
||||||
matrixClient.joinRoom(roomId)
|
joinRoom(roomId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,39 +25,14 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
open class RoomDirectorySearchStateProvider : PreviewParameterProvider<RoomDirectoryState> {
|
open class RoomDirectoryStateProvider : PreviewParameterProvider<RoomDirectoryState> {
|
||||||
override val values: Sequence<RoomDirectoryState>
|
override val values: Sequence<RoomDirectoryState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aRoomDirectoryState(),
|
aRoomDirectoryState(),
|
||||||
aRoomDirectoryState(
|
aRoomDirectoryState(
|
||||||
query = "Element",
|
query = "Element",
|
||||||
roomDescriptions = persistentListOf(
|
roomDescriptions = aRoomDescriptionList(),
|
||||||
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,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,10 +41,40 @@ fun aRoomDirectoryState(
|
||||||
displayLoadMoreIndicator: Boolean = false,
|
displayLoadMoreIndicator: Boolean = false,
|
||||||
roomDescriptions: ImmutableList<RoomDescription> = persistentListOf(),
|
roomDescriptions: ImmutableList<RoomDescription> = persistentListOf(),
|
||||||
joinRoomAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
joinRoomAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||||
|
eventSink: (RoomDirectoryEvents) -> Unit = {},
|
||||||
) = RoomDirectoryState(
|
) = RoomDirectoryState(
|
||||||
query = query,
|
query = query,
|
||||||
roomDescriptions = roomDescriptions,
|
roomDescriptions = roomDescriptions,
|
||||||
displayLoadMoreIndicator = displayLoadMoreIndicator,
|
displayLoadMoreIndicator = displayLoadMoreIndicator,
|
||||||
joinRoomAction = joinRoomAction,
|
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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
|
@ -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.TextField
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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 io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
|
@ -71,7 +73,6 @@ fun RoomDirectoryView(
|
||||||
onBackPressed: () -> Unit,
|
onBackPressed: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun joinRoom(roomId: RoomId) {
|
fun joinRoom(roomId: RoomId) {
|
||||||
state.eventSink(RoomDirectoryEvents.JoinRoom(roomId))
|
state.eventSink(RoomDirectoryEvents.JoinRoom(roomId))
|
||||||
}
|
}
|
||||||
|
|
@ -86,8 +87,8 @@ fun RoomDirectoryView(
|
||||||
state = state,
|
state = state,
|
||||||
onResultClicked = ::joinRoom,
|
onResultClicked = ::joinRoom,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.consumeWindowInsets(padding)
|
.consumeWindowInsets(padding)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -96,7 +97,8 @@ fun RoomDirectoryView(
|
||||||
onSuccess = onRoomJoined,
|
onSuccess = onRoomJoined,
|
||||||
onErrorDismiss = {
|
onErrorDismiss = {
|
||||||
state.eventSink(RoomDirectoryEvents.JoinRoomDismissError)
|
state.eventSink(RoomDirectoryEvents.JoinRoomDismissError)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
@ -171,7 +173,7 @@ private fun RoomDirectoryRoomList(
|
||||||
if (displayLoadMoreIndicator) {
|
if (displayLoadMoreIndicator) {
|
||||||
item {
|
item {
|
||||||
LoadMoreIndicator(modifier = Modifier.fillMaxWidth())
|
LoadMoreIndicator(modifier = Modifier.fillMaxWidth())
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(onReachedLoadMore) {
|
||||||
onReachedLoadMore()
|
onReachedLoadMore()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -182,10 +184,10 @@ private fun RoomDirectoryRoomList(
|
||||||
@Composable
|
@Composable
|
||||||
private fun LoadMoreIndicator(modifier: Modifier = Modifier) {
|
private fun LoadMoreIndicator(modifier: Modifier = Modifier) {
|
||||||
Box(
|
Box(
|
||||||
modifier
|
modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
.padding(24.dp),
|
.padding(24.dp),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
|
|
@ -213,7 +215,7 @@ private fun SearchTextField(
|
||||||
) {
|
) {
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
TextField(
|
TextField(
|
||||||
modifier = modifier,
|
modifier = modifier.testTag(TestTags.searchTextField.value),
|
||||||
textStyle = ElementTheme.typography.fontBodyLgRegular,
|
textStyle = ElementTheme.typography.fontBodyLgRegular,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
value = query,
|
value = query,
|
||||||
|
|
@ -255,14 +257,14 @@ private fun RoomDirectoryRoomRow(
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable { onClick(roomDescription.roomId) }
|
.clickable { onClick(roomDescription.roomId) }
|
||||||
.padding(
|
.padding(
|
||||||
top = 12.dp,
|
top = 12.dp,
|
||||||
bottom = 12.dp,
|
bottom = 12.dp,
|
||||||
start = 16.dp,
|
start = 16.dp,
|
||||||
)
|
)
|
||||||
.height(IntrinsicSize.Min),
|
.height(IntrinsicSize.Min),
|
||||||
) {
|
) {
|
||||||
Avatar(
|
Avatar(
|
||||||
avatarData = roomDescription.avatarData,
|
avatarData = roomDescription.avatarData,
|
||||||
|
|
@ -270,8 +272,8 @@ private fun RoomDirectoryRoomRow(
|
||||||
)
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(start = 16.dp)
|
.padding(start = 16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = roomDescription.name,
|
text = roomDescription.name,
|
||||||
|
|
@ -293,8 +295,8 @@ private fun RoomDirectoryRoomRow(
|
||||||
text = stringResource(id = CommonStrings.action_join),
|
text = stringResource(id = CommonStrings.action_join),
|
||||||
color = ElementTheme.colors.textSuccessPrimary,
|
color = ElementTheme.colors.textSuccessPrimary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterVertically)
|
.align(Alignment.CenterVertically)
|
||||||
.padding(start = 4.dp, end = 12.dp)
|
.padding(start = 4.dp, end = 12.dp)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Spacer(modifier = Modifier.width(24.dp))
|
Spacer(modifier = Modifier.width(24.dp))
|
||||||
|
|
@ -304,7 +306,7 @@ private fun RoomDirectoryRoomRow(
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomDirectorySearchViewLightPreview(@PreviewParameter(RoomDirectorySearchStateProvider::class) state: RoomDirectoryState) = ElementPreview {
|
internal fun RoomDirectoryViewPreview(@PreviewParameter(RoomDirectoryStateProvider::class) state: RoomDirectoryState) = ElementPreview {
|
||||||
RoomDirectoryView(
|
RoomDirectoryView(
|
||||||
state = state,
|
state = state,
|
||||||
onRoomJoined = {},
|
onRoomJoined = {},
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -24,7 +24,6 @@ internal data class RoomDirectoryListState(
|
||||||
val hasMoreToLoad: Boolean,
|
val hasMoreToLoad: Boolean,
|
||||||
val items: ImmutableList<RoomDescription>,
|
val items: ImmutableList<RoomDescription>,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val Default = RoomDirectoryListState(
|
val Default = RoomDirectoryListState(
|
||||||
hasMoreToLoad = true,
|
hasMoreToLoad = true,
|
||||||
|
|
|
||||||
|
|
@ -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 @@
|
||||||
|
/*
|
||||||
|
* 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 @@
|
||||||
|
/*
|
||||||
|
* 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -191,6 +191,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
|
||||||
onInvitesClicked: () -> Unit = EnsureNeverCalled(),
|
onInvitesClicked: () -> Unit = EnsureNeverCalled(),
|
||||||
onRoomSettingsClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
onRoomSettingsClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onMenuActionClicked: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(),
|
onMenuActionClicked: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
|
onRoomDirectorySearchClicked: () -> Unit = EnsureNeverCalled(),
|
||||||
) {
|
) {
|
||||||
setContent {
|
setContent {
|
||||||
RoomListView(
|
RoomListView(
|
||||||
|
|
@ -203,6 +204,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
|
||||||
onInvitesClicked = onInvitesClicked,
|
onInvitesClicked = onInvitesClicked,
|
||||||
onRoomSettingsClicked = onRoomSettingsClicked,
|
onRoomSettingsClicked = onRoomSettingsClicked,
|
||||||
onMenuActionClicked = onMenuActionClicked,
|
onMenuActionClicked = onMenuActionClicked,
|
||||||
|
onRoomDirectorySearchClicked = onRoomDirectorySearchClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
||||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
||||||
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
|
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.RoomListFilter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
|
|
@ -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(
|
fun TestScope.createRoomListSearchPresenter(
|
||||||
roomListService: RoomListService = FakeRoomListService(),
|
roomListService: RoomListService = FakeRoomListService(),
|
||||||
|
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||||
): RoomListSearchPresenter {
|
): RoomListSearchPresenter {
|
||||||
return RoomListSearchPresenter(
|
return RoomListSearchPresenter(
|
||||||
dataSource = RoomListSearchDataSource(
|
dataSource = RoomListSearchDataSource(
|
||||||
|
|
@ -141,6 +160,7 @@ fun TestScope.createRoomListSearchPresenter(
|
||||||
roomLastMessageFormatter = FakeRoomLastMessageFormatter(),
|
roomLastMessageFormatter = FakeRoomLastMessageFormatter(),
|
||||||
),
|
),
|
||||||
coroutineDispatchers = testCoroutineDispatchers(),
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
)
|
),
|
||||||
|
featureFlagService = featureFlagService,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
@ -441,7 +440,7 @@ class RustMatrixClient(
|
||||||
runCatching { client.removeAvatar() }
|
runCatching { client.removeAvatar() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun joinRoom(roomId: RoomId): Result<RoomId> = withContext(sessionDispatcher) {
|
override suspend fun joinRoom(roomId: RoomId): Result<RoomId> = withContext(sessionDispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
client.joinRoomById(roomId.value).destroy()
|
client.joinRoomById(roomId.value).destroy()
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import org.matrix.rustcomponents.sdk.PublicRoomJoinRule
|
||||||
import org.matrix.rustcomponents.sdk.RoomDescription as RustRoomDescription
|
import org.matrix.rustcomponents.sdk.RoomDescription as RustRoomDescription
|
||||||
|
|
||||||
class RoomDescriptionMapper {
|
class RoomDescriptionMapper {
|
||||||
|
|
||||||
fun map(roomDescription: RustRoomDescription): RoomDescription {
|
fun map(roomDescription: RustRoomDescription): RoomDescription {
|
||||||
return RoomDescription(
|
return RoomDescription(
|
||||||
roomId = RoomId(roomDescription.roomId),
|
roomId = RoomId(roomDescription.roomId),
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ class RoomDirectorySearchProcessor(
|
||||||
private val coroutineContext: CoroutineContext,
|
private val coroutineContext: CoroutineContext,
|
||||||
private val roomDescriptionMapper: RoomDescriptionMapper,
|
private val roomDescriptionMapper: RoomDescriptionMapper,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
||||||
suspend fun postUpdates(updates: List<RoomDirectorySearchEntryUpdate>) {
|
suspend fun postUpdates(updates: List<RoomDirectorySearchEntryUpdate>) {
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ class RustRoomDirectoryList(
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
private val coroutineContext: CoroutineContext,
|
private val coroutineContext: CoroutineContext,
|
||||||
) : RoomDirectoryList {
|
) : RoomDirectoryList {
|
||||||
|
|
||||||
private val hasMoreToLoad = MutableStateFlow(true)
|
private val hasMoreToLoad = MutableStateFlow(true)
|
||||||
private val items = MutableSharedFlow<List<RoomDescription>>(replay = 1)
|
private val items = MutableSharedFlow<List<RoomDescription>>(replay = 1)
|
||||||
private val processor = RoomDirectorySearchProcessor(items, coroutineContext, RoomDescriptionMapper())
|
private val processor = RoomDirectorySearchProcessor(items, coroutineContext, RoomDescriptionMapper())
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ class RustRoomDirectoryService(
|
||||||
private val client: Client,
|
private val client: Client,
|
||||||
private val sessionDispatcher: CoroutineDispatcher,
|
private val sessionDispatcher: CoroutineDispatcher,
|
||||||
) : RoomDirectoryService {
|
) : RoomDirectoryService {
|
||||||
|
|
||||||
override fun createRoomDirectoryList(scope: CoroutineScope): RoomDirectoryList {
|
override fun createRoomDirectoryList(scope: CoroutineScope): RoomDirectoryList {
|
||||||
return RustRoomDirectoryList(client.roomDirectorySearch(), scope, sessionDispatcher)
|
return RustRoomDirectoryList(client.roomDirectorySearch(), scope, sessionDispatcher)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,9 @@ class FakeMatrixClient(
|
||||||
private var setDisplayNameResult: Result<Unit> = Result.success(Unit)
|
private var setDisplayNameResult: Result<Unit> = Result.success(Unit)
|
||||||
private var uploadAvatarResult: Result<Unit> = Result.success(Unit)
|
private var uploadAvatarResult: Result<Unit> = Result.success(Unit)
|
||||||
private var removeAvatarResult: 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? {
|
override suspend fun getRoom(roomId: RoomId): MatrixRoom? {
|
||||||
return getRoomResults[roomId]
|
return getRoomResults[roomId]
|
||||||
|
|
@ -181,6 +184,8 @@ class FakeMatrixClient(
|
||||||
return removeAvatarResult
|
return removeAvatarResult
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun joinRoom(roomId: RoomId): Result<RoomId> = joinRoomLambda(roomId)
|
||||||
|
|
||||||
override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService
|
override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService
|
||||||
|
|
||||||
override fun pushersService(): PushersService = pushersService
|
override fun pushersService(): PushersService = pushersService
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -20,8 +20,8 @@ import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
|
||||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
class FakeRoomDirectoryService : RoomDirectoryService {
|
class FakeRoomDirectoryService(
|
||||||
override fun createRoomDirectoryList(scope: CoroutineScope): RoomDirectoryList {
|
private val createRoomDirectoryListFactory: (CoroutineScope) -> RoomDirectoryList = { throw AssertionError("Configure a proper factory.") }
|
||||||
TODO("Not yet implemented")
|
) : RoomDirectoryService {
|
||||||
}
|
override fun createRoomDirectoryList(scope: CoroutineScope) = createRoomDirectoryListFactory(scope)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -100,4 +100,9 @@ object TestTags {
|
||||||
* Timeline item.
|
* Timeline item.
|
||||||
*/
|
*/
|
||||||
val timelineItemSenderInfo = TestTag("timeline_item-sender_info")
|
val timelineItemSenderInfo = TestTag("timeline_item-sender_info")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search field.
|
||||||
|
*/
|
||||||
|
val searchTextField = TestTag("search_text_field")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,8 @@ class RoomListScreen(
|
||||||
roomListService = matrixClient.roomListService,
|
roomListService = matrixClient.roomListService,
|
||||||
roomSummaryFactory = roomListRoomSummaryFactory,
|
roomSummaryFactory = roomListRoomSummaryFactory,
|
||||||
coroutineDispatchers = coroutineDispatchers,
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
)
|
),
|
||||||
|
featureFlagService = featureFlagService,
|
||||||
),
|
),
|
||||||
sessionPreferencesStore = DefaultSessionPreferencesStore(
|
sessionPreferencesStore = DefaultSessionPreferencesStore(
|
||||||
context = context,
|
context = context,
|
||||||
|
|
@ -156,6 +157,7 @@ class RoomListScreen(
|
||||||
onInvitesClicked = {},
|
onInvitesClicked = {},
|
||||||
onRoomSettingsClicked = {},
|
onRoomSettingsClicked = {},
|
||||||
onMenuActionClicked = {},
|
onMenuActionClicked = {},
|
||||||
|
onRoomDirectorySearchClicked = {},
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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…
Add table
Add a link
Reference in a new issue