create room : fix tests and remove auto-fill alias for now.

This commit is contained in:
ganfra 2024-11-04 15:35:33 +01:00
parent 5255b03aca
commit 06bea47864
10 changed files with 169 additions and 123 deletions

View file

@ -46,6 +46,7 @@ class CreateRoomDataStore @Inject constructor(
fun setRoomName(roomName: String) { fun setRoomName(roomName: String) {
createRoomConfigFlow.getAndUpdate { config -> createRoomConfigFlow.getAndUpdate { config ->
/*
val newVisibility = when (config.roomVisibility) { val newVisibility = when (config.roomVisibility) {
is RoomVisibilityState.Public -> { is RoomVisibilityState.Public -> {
val roomAddress = config.roomVisibility.roomAddress val roomAddress = config.roomVisibility.roomAddress
@ -59,9 +60,9 @@ class CreateRoomDataStore @Inject constructor(
} }
else -> config.roomVisibility else -> config.roomVisibility
} }
*/
config.copy( config.copy(
roomName = roomName.takeIf { it.isNotEmpty() }, roomName = roomName.takeIf { it.isNotEmpty() },
roomVisibility = newVisibility,
) )
} }
} }

View file

@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectable
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -36,13 +35,13 @@ fun RoomAccessOption(
isSelected: Boolean = false, isSelected: Boolean = false,
) { ) {
Row( Row(
modifier modifier
.fillMaxWidth() .fillMaxWidth()
.selectable( .selectable(
selected = isSelected, selected = isSelected,
onClick = { onOptionClick(roomAccessItem) }, onClick = { onOptionClick(roomAccessItem) },
role = Role.RadioButton, role = Role.RadioButton,
) )
) { ) {
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
Text( Text(
@ -59,8 +58,8 @@ fun RoomAccessOption(
} }
RadioButton( RadioButton(
modifier = Modifier modifier = Modifier
.align(Alignment.CenterVertically) .align(Alignment.CenterVertically)
.size(48.dp), .size(48.dp),
selected = isSelected, selected = isSelected,
// null recommended for accessibility with screenreaders // null recommended for accessibility with screenreaders
onClick = null onClick = null

View file

@ -50,7 +50,7 @@ fun RoomVisibilityOption(
) )
) { ) {
Box( Box(
modifier = modifier modifier = Modifier
.size(30.dp) .size(30.dp)
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
.background(ElementTheme.colors.bgSubtleSecondary) .background(ElementTheme.colors.bgSubtleSecondary)
@ -60,7 +60,7 @@ fun RoomVisibilityOption(
Icon( Icon(
resourceId = roomPrivacyItem.icon, resourceId = roomPrivacyItem.icon,
contentDescription = null, contentDescription = null,
tint = if(isSelected) ElementTheme.colors.iconPrimary else ElementTheme.colors.iconSecondary, tint = if (isSelected) ElementTheme.colors.iconPrimary else ElementTheme.colors.iconSecondary,
) )
} }
Spacer(Modifier.size(16.dp)) Spacer(Modifier.size(16.dp))

View file

@ -7,7 +7,6 @@
package io.element.android.features.createroom.impl.configureroom package io.element.android.features.createroom.impl.configureroom
import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.matrix.ui.media.AvatarAction
@ -18,7 +17,7 @@ sealed interface ConfigureRoomEvents {
data class RoomAccessChanged(val roomAccess: RoomAccessItem) : ConfigureRoomEvents data class RoomAccessChanged(val roomAccess: RoomAccessItem) : ConfigureRoomEvents
data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvents data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvents
data class RemoveUserFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents data class RemoveUserFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents
data class CreateRoom(val config: CreateRoomConfig) : ConfigureRoomEvents data object CreateRoom : ConfigureRoomEvents
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
data object CancelCreateRoom : ConfigureRoomEvents data object CancelCreateRoom : ConfigureRoomEvents
} }

View file

@ -22,14 +22,13 @@ import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.features.createroom.impl.CreateRoomDataStore
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.runCatchingUpdatingState
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient 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.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.createroom.JoinRuleOverride
import io.element.android.libraries.matrix.api.createroom.RoomPreset import io.element.android.libraries.matrix.api.createroom.RoomPreset
import io.element.android.libraries.matrix.api.createroom.RoomVisibility import io.element.android.libraries.matrix.api.createroom.RoomVisibility
import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.matrix.ui.media.AvatarAction
@ -61,7 +60,7 @@ class ConfigureRoomPresenter @Inject constructor(
val cameraPermissionState = cameraPermissionPresenter.present() val cameraPermissionState = cameraPermissionPresenter.present()
val createRoomConfig = dataStore.createRoomConfig.collectAsState(CreateRoomConfig()) val createRoomConfig = dataStore.createRoomConfig.collectAsState(CreateRoomConfig())
val homeserverName = remember { matrixClient.userIdServerName() } val homeserverName = remember { matrixClient.userIdServerName() }
val isKnockFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock).collectAsState(initial = false) val isKnockFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock).collectAsState(initial = false)
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker( val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri, cached = true) }, onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri, cached = true) },
@ -103,7 +102,7 @@ class ConfigureRoomPresenter @Inject constructor(
is ConfigureRoomEvents.RemoveUserFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser) is ConfigureRoomEvents.RemoveUserFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser)
is ConfigureRoomEvents.RoomAccessChanged -> dataStore.setRoomAccess(event.roomAccess) is ConfigureRoomEvents.RoomAccessChanged -> dataStore.setRoomAccess(event.roomAccess)
is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress) is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
is ConfigureRoomEvents.CreateRoom -> createRoom(event.config) is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig.value)
is ConfigureRoomEvents.HandleAvatarAction -> { is ConfigureRoomEvents.HandleAvatarAction -> {
when (event.action) { when (event.action) {
AvatarAction.ChoosePhoto -> galleryImagePicker.launch() AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
@ -118,7 +117,6 @@ class ConfigureRoomPresenter @Inject constructor(
} }
ConfigureRoomEvents.CancelCreateRoom -> createRoomAction.value = AsyncAction.Uninitialized ConfigureRoomEvents.CancelCreateRoom -> createRoomAction.value = AsyncAction.Uninitialized
} }
} }
@ -137,20 +135,33 @@ class ConfigureRoomPresenter @Inject constructor(
config: CreateRoomConfig, config: CreateRoomConfig,
createRoomAction: MutableState<AsyncAction<RoomId>> createRoomAction: MutableState<AsyncAction<RoomId>>
) = launch { ) = launch {
runUpdatingState(createRoomAction) { suspend {
val avatarUrl = config.avatarUri?.let { uploadAvatar(it) } val avatarUrl = config.avatarUri?.let { uploadAvatar(it) }
val params = CreateRoomParameters( val params = if (config.roomVisibility is RoomVisibilityState.Public) {
name = config.roomName, CreateRoomParameters(
topic = config.topic, name = config.roomName,
isEncrypted = config.roomVisibility is RoomVisibilityState.Private, topic = config.topic,
isDirect = false, isEncrypted = false,
visibility = if (config.roomVisibility is RoomVisibilityState.Public) RoomVisibility.PUBLIC else RoomVisibility.PRIVATE, isDirect = false,
joinRuleOverride = if (config.roomVisibility is RoomVisibilityState.Public) config.roomVisibility.roomAccess.toJoinRule() else JoinRuleOverride.None, visibility = RoomVisibility.PUBLIC,
preset = if (config.roomVisibility is RoomVisibilityState.Public) RoomPreset.PUBLIC_CHAT else RoomPreset.PRIVATE_CHAT, joinRuleOverride = config.roomVisibility.roomAccess.toJoinRule(),
invite = config.invites.map { it.userId }, preset = RoomPreset.PUBLIC_CHAT,
avatar = avatarUrl, invite = config.invites.map { it.userId },
canonicalAlias = config.roomVisibility.roomAddress() avatar = avatarUrl,
) canonicalAlias = config.roomVisibility.roomAddress()
)
} else {
CreateRoomParameters(
name = config.roomName,
topic = config.topic,
isEncrypted = config.roomVisibility is RoomVisibilityState.Private,
isDirect = false,
visibility = RoomVisibility.PRIVATE,
preset = RoomPreset.PRIVATE_CHAT,
invite = config.invites.map { it.userId },
avatar = avatarUrl,
)
}
matrixClient.createRoom(params) matrixClient.createRoom(params)
.onFailure { failure -> .onFailure { failure ->
Timber.e(failure, "Failed to create room") Timber.e(failure, "Failed to create room")
@ -159,7 +170,8 @@ class ConfigureRoomPresenter @Inject constructor(
dataStore.clearCachedData() dataStore.clearCachedData()
analyticsService.capture(CreatedRoom(isDM = false)) analyticsService.capture(CreatedRoom(isDM = false))
} }
} .getOrThrow()
}.runCatchingUpdatingState(createRoomAction)
} }
private suspend fun uploadAvatar(avatarUri: Uri): String { private suspend fun uploadAvatar(avatarUri: Uri): String {

View file

@ -15,7 +15,6 @@ import io.element.android.libraries.matrix.ui.components.aMatrixUserList
import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.permissions.api.PermissionsState import io.element.android.libraries.permissions.api.PermissionsState
import io.element.android.libraries.permissions.api.aPermissionsState import io.element.android.libraries.permissions.api.aPermissionsState
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> { open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
@ -23,6 +22,7 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
get() = sequenceOf( get() = sequenceOf(
aConfigureRoomState(), aConfigureRoomState(),
aConfigureRoomState( aConfigureRoomState(
isKnockFeatureEnabled = false,
config = CreateRoomConfig( config = CreateRoomConfig(
roomName = "Room 101", roomName = "Room 101",
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines", topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
@ -35,7 +35,6 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
), ),
), ),
aConfigureRoomState( aConfigureRoomState(
isKnockFeatureEnabled = false,
config = CreateRoomConfig( config = CreateRoomConfig(
roomName = "Room 101", roomName = "Room 101",
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines", topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",

View file

@ -44,7 +44,6 @@ import io.element.android.libraries.designsystem.components.async.AsyncActionVie
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
@ -82,7 +81,7 @@ fun ConfigureRoomView(
onBackClick = onBackClick, onBackClick = onBackClick,
onNextClick = { onNextClick = {
focusManager.clearFocus() focusManager.clearFocus()
state.eventSink(ConfigureRoomEvents.CreateRoom(state.config)) state.eventSink(ConfigureRoomEvents.CreateRoom)
}, },
) )
} }
@ -164,7 +163,7 @@ fun ConfigureRoomView(
}, },
onSuccess = { onCreateRoomSuccess(it) }, onSuccess = { onCreateRoomSuccess(it) },
errorMessage = { stringResource(R.string.screen_create_room_error_creating_room) }, errorMessage = { stringResource(R.string.screen_create_room_error_creating_room) },
onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom(state.config)) }, onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom) },
onErrorDismiss = { state.eventSink(ConfigureRoomEvents.CancelCreateRoom) }, onErrorDismiss = { state.eventSink(ConfigureRoomEvents.CancelCreateRoom) },
) )
@ -326,7 +325,7 @@ private fun RoomAddressField(
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
text = "Room address", text = "Room address",
) )
TextField( TextField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
value = address.value, value = address.value,
@ -359,7 +358,6 @@ private fun RoomAddressField(
} }
@PreviewsDayNight @PreviewsDayNight
@PreviewWithLargeHeight
@Composable @Composable
internal fun ConfigureRoomViewPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = ElementPreview { internal fun ConfigureRoomViewPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = ElementPreview {
ConfigureRoomView( ConfigureRoomView(

View file

@ -10,7 +10,8 @@ package io.element.android.features.createroom.impl.configureroom
import io.element.android.libraries.matrix.api.createroom.JoinRuleOverride import io.element.android.libraries.matrix.api.createroom.JoinRuleOverride
enum class RoomAccess { enum class RoomAccess {
Anyone, Knocking Anyone,
Knocking
} }
fun RoomAccess.toJoinRule(): JoinRuleOverride { fun RoomAccess.toJoinRule(): JoinRuleOverride {

View file

@ -23,4 +23,3 @@ enum class RoomAccessItem(
description = CommonStrings.screen_create_room_access_section_knocking_option_description, description = CommonStrings.screen_create_room_access_section_knocking_option_description,
), ),
} }

View file

@ -8,15 +8,16 @@
package io.element.android.features.createroom.impl.configureroom package io.element.android.features.createroom.impl.configureroom
import android.net.Uri import android.net.Uri
import app.cash.molecule.RecompositionMode import app.cash.turbine.TurbineTestContext
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.CreatedRoom import im.vector.app.features.analytics.plan.CreatedRoom
import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.features.createroom.impl.CreateRoomDataStore
import io.element.android.features.createroom.impl.userlist.UserListDataStore import io.element.android.features.createroom.impl.userlist.UserListDataStore
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
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.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_MESSAGE
@ -25,13 +26,18 @@ import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediapickers.test.FakePickerProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.mediaupload.api.MediaUploadInfo
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.mockkStatic
@ -56,33 +62,8 @@ class ConfigureRoomPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
private lateinit var presenter: ConfigureRoomPresenter
private lateinit var userListDataStore: UserListDataStore
private lateinit var createRoomDataStore: CreateRoomDataStore
private lateinit var fakeMatrixClient: FakeMatrixClient
private lateinit var fakePickerProvider: FakePickerProvider
private lateinit var fakeMediaPreProcessor: FakeMediaPreProcessor
private lateinit var fakeAnalyticsService: FakeAnalyticsService
private lateinit var fakePermissionsPresenter: FakePermissionsPresenter
@Before @Before
fun setup() { fun setup() {
fakeMatrixClient = FakeMatrixClient()
userListDataStore = UserListDataStore()
createRoomDataStore = CreateRoomDataStore(userListDataStore)
fakePickerProvider = FakePickerProvider()
fakeMediaPreProcessor = FakeMediaPreProcessor()
fakeAnalyticsService = FakeAnalyticsService()
fakePermissionsPresenter = FakePermissionsPresenter()
presenter = ConfigureRoomPresenter(
dataStore = createRoomDataStore,
matrixClient = fakeMatrixClient,
mediaPickerProvider = fakePickerProvider,
mediaPreProcessor = fakeMediaPreProcessor,
analyticsService = fakeAnalyticsService,
permissionsPresenterFactory = FakePermissionsPresenterFactory(fakePermissionsPresenter),
)
mockkStatic(File::readBytes) mockkStatic(File::readBytes)
every { any<File>().readBytes() } returns byteArrayOf() every { any<File>().readBytes() } returns byteArrayOf()
} }
@ -94,50 +75,56 @@ class ConfigureRoomPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
moleculeFlow(RecompositionMode.Immediate) { val presenter = createConfigureRoomPresenter()
presenter.present() presenter.test {
}.test { val initialState = initialState()
val initialState = awaitItem()
assertThat(initialState.config).isEqualTo(CreateRoomConfig()) assertThat(initialState.config).isEqualTo(CreateRoomConfig())
assertThat(initialState.config.roomName).isNull() assertThat(initialState.config.roomName).isNull()
assertThat(initialState.config.topic).isNull() assertThat(initialState.config.topic).isNull()
assertThat(initialState.config.invites).isEmpty() assertThat(initialState.config.invites).isEmpty()
assertThat(initialState.config.avatarUri).isNull() assertThat(initialState.config.avatarUri).isNull()
assertThat(initialState.config.roomVisibility).isEqualTo(RoomVisibilityState.Private) assertThat(initialState.config.roomVisibility).isEqualTo(RoomVisibilityState.Private)
assertThat(initialState.createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(initialState.homeserverName).isEqualTo("matrix.org")
} }
} }
@Test @Test
fun `present - create room button is enabled only if the required fields are completed`() = runTest { fun `present - create room button is enabled only if the required fields are completed`() = runTest {
moleculeFlow(RecompositionMode.Immediate) { val presenter = createConfigureRoomPresenter()
presenter.present() presenter.test {
}.test { val initialState = initialState()
val initialState = awaitItem()
var config = initialState.config var config = initialState.config
assertThat(initialState.isCreateButtonEnabled).isFalse() assertThat(initialState.config.isValid).isFalse()
// Room name not empty // Room name not empty
initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME)) initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
var newState: ConfigureRoomState = awaitItem() var newState: ConfigureRoomState = awaitItem()
config = config.copy(roomName = A_ROOM_NAME) config = config.copy(roomName = A_ROOM_NAME)
assertThat(newState.config).isEqualTo(config) assertThat(newState.config).isEqualTo(config)
assertThat(newState.isCreateButtonEnabled).isTrue() assertThat(newState.config.isValid).isTrue()
// Clear room name // Clear room name
newState.eventSink(ConfigureRoomEvents.RoomNameChanged("")) newState.eventSink(ConfigureRoomEvents.RoomNameChanged(""))
newState = awaitItem() newState = awaitItem()
config = config.copy(roomName = null) config = config.copy(roomName = null)
assertThat(newState.config).isEqualTo(config) assertThat(newState.config).isEqualTo(config)
assertThat(newState.isCreateButtonEnabled).isFalse() assertThat(newState.config.isValid).isFalse()
} }
} }
@Test @Test
fun `present - state is updated when fields are changed`() = runTest { fun `present - state is updated when fields are changed`() = runTest {
moleculeFlow(RecompositionMode.Immediate) { val userListDataStore = UserListDataStore()
presenter.present() val pickerProvider = FakePickerProvider()
}.test { val permissionsPresenter = FakePermissionsPresenter()
val initialState = awaitItem() val presenter = createConfigureRoomPresenter(
createRoomDataStore = CreateRoomDataStore(userListDataStore),
pickerProvider = pickerProvider,
permissionsPresenter = permissionsPresenter,
)
presenter.test {
val initialState = initialState()
var expectedConfig = CreateRoomConfig() var expectedConfig = CreateRoomConfig()
assertThat(initialState.config).isEqualTo(expectedConfig) assertThat(initialState.config).isEqualTo(expectedConfig)
@ -165,22 +152,22 @@ class ConfigureRoomPresenterTest {
// Room avatar // Room avatar
// Pick avatar // Pick avatar
fakePickerProvider.givenResult(null) pickerProvider.givenResult(null)
// From gallery // From gallery
val uriFromGallery = Uri.parse(AN_URI_FROM_GALLERY) val uriFromGallery = Uri.parse(AN_URI_FROM_GALLERY)
fakePickerProvider.givenResult(uriFromGallery) pickerProvider.givenResult(uriFromGallery)
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
newState = awaitItem() newState = awaitItem()
expectedConfig = expectedConfig.copy(avatarUri = uriFromGallery) expectedConfig = expectedConfig.copy(avatarUri = uriFromGallery)
assertThat(newState.config).isEqualTo(expectedConfig) assertThat(newState.config).isEqualTo(expectedConfig)
// From camera // From camera
val uriFromCamera = Uri.parse(AN_URI_FROM_CAMERA) val uriFromCamera = Uri.parse(AN_URI_FROM_CAMERA)
fakePickerProvider.givenResult(uriFromCamera) pickerProvider.givenResult(uriFromCamera)
assertThat(newState.cameraPermissionState.permissionGranted).isFalse() assertThat(newState.cameraPermissionState.permissionGranted).isFalse()
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto)) newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
newState = awaitItem() newState = awaitItem()
assertThat(newState.cameraPermissionState.showDialog).isTrue() assertThat(newState.cameraPermissionState.showDialog).isTrue()
fakePermissionsPresenter.setPermissionGranted() permissionsPresenter.setPermissionGranted()
newState = awaitItem() newState = awaitItem()
assertThat(newState.cameraPermissionState.permissionGranted).isTrue() assertThat(newState.cameraPermissionState.permissionGranted).isTrue()
newState = awaitItem() newState = awaitItem()
@ -188,7 +175,7 @@ class ConfigureRoomPresenterTest {
assertThat(newState.config).isEqualTo(expectedConfig) assertThat(newState.config).isEqualTo(expectedConfig)
// Do it again, no permission is requested // Do it again, no permission is requested
val uriFromCamera2 = Uri.parse(AN_URI_FROM_CAMERA_2) val uriFromCamera2 = Uri.parse(AN_URI_FROM_CAMERA_2)
fakePickerProvider.givenResult(uriFromCamera2) pickerProvider.givenResult(uriFromCamera2)
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto)) newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
newState = awaitItem() newState = awaitItem()
expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera2) expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera2)
@ -200,9 +187,15 @@ class ConfigureRoomPresenterTest {
assertThat(newState.config).isEqualTo(expectedConfig) assertThat(newState.config).isEqualTo(expectedConfig)
// Room privacy // Room privacy
newState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityState.Public)) newState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
newState = awaitItem() newState = awaitItem()
expectedConfig = expectedConfig.copy(roomVisibility = RoomVisibilityState.Public) expectedConfig = expectedConfig.copy(
roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled(expectedConfig.roomName ?: ""),
roomAddressErrorState = RoomAddressErrorState.None,
roomAccess = RoomAccess.Anyone,
)
)
assertThat(newState.config).isEqualTo(expectedConfig) assertThat(newState.config).isEqualTo(expectedConfig)
// Remove user // Remove user
@ -215,15 +208,17 @@ class ConfigureRoomPresenterTest {
@Test @Test
fun `present - trigger create room action`() = runTest { fun `present - trigger create room action`() = runTest {
moleculeFlow(RecompositionMode.Immediate) { val matrixClient = createMatrixClient()
presenter.present() val presenter = createConfigureRoomPresenter(
}.test { matrixClient = matrixClient
val initialState = awaitItem() )
presenter.test {
val initialState = initialState()
val createRoomResult = Result.success(RoomId("!createRoomResult:domain")) val createRoomResult = Result.success(RoomId("!createRoomResult:domain"))
fakeMatrixClient.givenCreateRoomResult(createRoomResult) matrixClient.givenCreateRoomResult(createRoomResult)
initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config)) initialState.eventSink(ConfigureRoomEvents.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java) assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterCreateRoom = awaitItem() val stateAfterCreateRoom = awaitItem()
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Success::class.java) assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
@ -233,18 +228,22 @@ class ConfigureRoomPresenterTest {
@Test @Test
fun `present - record analytics when creating room`() = runTest { fun `present - record analytics when creating room`() = runTest {
moleculeFlow(RecompositionMode.Immediate) { val matrixClient = createMatrixClient()
presenter.present() val analyticsService = FakeAnalyticsService()
}.test { val presenter = createConfigureRoomPresenter(
val initialState = awaitItem() matrixClient = matrixClient,
analyticsService = analyticsService
)
presenter.test {
val initialState = initialState()
val createRoomResult = Result.success(RoomId("!createRoomResult:domain")) val createRoomResult = Result.success(RoomId("!createRoomResult:domain"))
fakeMatrixClient.givenCreateRoomResult(createRoomResult) matrixClient.givenCreateRoomResult(createRoomResult)
initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config)) initialState.eventSink(ConfigureRoomEvents.CreateRoom)
skipItems(2) skipItems(2)
val analyticsEvent = fakeAnalyticsService.capturedEvents.filterIsInstance<CreatedRoom>().firstOrNull() val analyticsEvent = analyticsService.capturedEvents.filterIsInstance<CreatedRoom>().firstOrNull()
assertThat(analyticsEvent).isNotNull() assertThat(analyticsEvent).isNotNull()
assertThat(analyticsEvent?.isDM).isFalse() assertThat(analyticsEvent?.isDM).isFalse()
} }
@ -252,23 +251,31 @@ class ConfigureRoomPresenterTest {
@Test @Test
fun `present - trigger create room with upload error and retry`() = runTest { fun `present - trigger create room with upload error and retry`() = runTest {
moleculeFlow(RecompositionMode.Immediate) { val matrixClient = createMatrixClient()
presenter.present() val analyticsService = FakeAnalyticsService()
}.test { val mediaPreProcessor = FakeMediaPreProcessor()
skipItems(1) val createRoomDataStore = CreateRoomDataStore(UserListDataStore())
val presenter = createConfigureRoomPresenter(
createRoomDataStore = createRoomDataStore,
mediaPreProcessor = mediaPreProcessor,
matrixClient = matrixClient,
analyticsService = analyticsService
)
presenter.test {
val initialState = initialState()
createRoomDataStore.setAvatarUri(Uri.parse(AN_URI_FROM_GALLERY)) createRoomDataStore.setAvatarUri(Uri.parse(AN_URI_FROM_GALLERY))
fakeMediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(mockk(), mockk(), mockk()))) skipItems(1)
fakeMatrixClient.givenUploadMediaResult(Result.failure(A_THROWABLE)) mediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(mockk(), mockk(), mockk())))
matrixClient.givenUploadMediaResult(Result.failure(A_THROWABLE))
val initialState = awaitItem() initialState.eventSink(ConfigureRoomEvents.CreateRoom)
initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java) assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterCreateRoom = awaitItem() val stateAfterCreateRoom = awaitItem()
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java) assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
assertThat(fakeAnalyticsService.capturedEvents.filterIsInstance<CreatedRoom>()).isEmpty() assertThat(analyticsService.capturedEvents.filterIsInstance<CreatedRoom>()).isEmpty()
fakeMatrixClient.givenUploadMediaResult(Result.success(AN_AVATAR_URL)) matrixClient.givenUploadMediaResult(Result.success(AN_AVATAR_URL))
stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config)) stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java) assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java) assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Success::class.java) assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
@ -277,23 +284,25 @@ class ConfigureRoomPresenterTest {
@Test @Test
fun `present - trigger retry and cancel actions`() = runTest { fun `present - trigger retry and cancel actions`() = runTest {
moleculeFlow(RecompositionMode.Immediate) { val fakeMatrixClient = createMatrixClient()
presenter.present() val presenter = createConfigureRoomPresenter(
}.test { matrixClient = fakeMatrixClient
val initialState = awaitItem() )
presenter.test {
val initialState = initialState()
val createRoomResult = Result.failure<RoomId>(A_THROWABLE) val createRoomResult = Result.failure<RoomId>(A_THROWABLE)
fakeMatrixClient.givenCreateRoomResult(createRoomResult) fakeMatrixClient.givenCreateRoomResult(createRoomResult)
// Create // Create
initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config)) initialState.eventSink(ConfigureRoomEvents.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java) assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterCreateRoom = awaitItem() val stateAfterCreateRoom = awaitItem()
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java) assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
assertThat((stateAfterCreateRoom.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull()) assertThat((stateAfterCreateRoom.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
// Retry // Retry
stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config)) stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java) assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java) assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterRetry = awaitItem() val stateAfterRetry = awaitItem()
@ -305,4 +314,33 @@ class ConfigureRoomPresenterTest {
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java) assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
} }
} }
private suspend fun TurbineTestContext<ConfigureRoomState>.initialState(): ConfigureRoomState {
skipItems(1)
return awaitItem()
}
private fun createMatrixClient() = FakeMatrixClient(
userIdServerNameLambda = { "matrix.org" },
)
private fun createConfigureRoomPresenter(
createRoomDataStore: CreateRoomDataStore = CreateRoomDataStore(UserListDataStore()),
matrixClient: MatrixClient = createMatrixClient(),
pickerProvider: PickerProvider = FakePickerProvider(),
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
analyticsService: AnalyticsService = FakeAnalyticsService(),
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
isKnockFeatureEnabled: Boolean = true,
) = ConfigureRoomPresenter(
dataStore = createRoomDataStore,
matrixClient = matrixClient,
mediaPickerProvider = pickerProvider,
mediaPreProcessor = mediaPreProcessor,
analyticsService = analyticsService,
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter),
featureFlagService = FakeFeatureFlagService(
mapOf(FeatureFlags.Knock.key to isKnockFeatureEnabled)
)
)
} }