mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 16:10:00 +00:00
Add UserStatusPresenter with full test coverage
This commit is contained in:
+63
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.preferences.impl.userstatus
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
class UserStatusPresenter(
|
||||||
|
private val matrixClient: MatrixClient,
|
||||||
|
) : Presenter<UserStatusState> {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): UserStatusState {
|
||||||
|
val userProfile by matrixClient.userProfile.collectAsState()
|
||||||
|
var pickerState by remember { mutableStateOf<UserStatusPickerState>(UserStatusPickerState.Hidden) }
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
fun handleEvent(event: UserStatusEvent) {
|
||||||
|
when (event) {
|
||||||
|
UserStatusEvent.Open -> pickerState = UserStatusPickerState.ShowingPicker
|
||||||
|
UserStatusEvent.Dismiss -> pickerState = UserStatusPickerState.Hidden
|
||||||
|
UserStatusEvent.OpenCustomInput -> {
|
||||||
|
val raw = userProfile.rawStatus
|
||||||
|
pickerState = UserStatusPickerState.CustomInput(
|
||||||
|
initialEmoji = raw?.emoji ?: "😀",
|
||||||
|
initialText = raw?.text ?: "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is UserStatusEvent.Set -> {
|
||||||
|
pickerState = UserStatusPickerState.Hidden
|
||||||
|
coroutineScope.launch { matrixClient.setUserStatus(event.status) }
|
||||||
|
}
|
||||||
|
UserStatusEvent.Clear -> {
|
||||||
|
pickerState = UserStatusPickerState.Hidden
|
||||||
|
coroutineScope.launch { matrixClient.clearUserStatus() }
|
||||||
|
}
|
||||||
|
UserStatusEvent.CancelCustomInput -> pickerState = UserStatusPickerState.Hidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return UserStatusState(
|
||||||
|
displayedStatus = userProfile.displayedStatus,
|
||||||
|
pickerState = pickerState,
|
||||||
|
eventSink = ::handleEvent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.preferences.impl.userstatus
|
||||||
|
|
||||||
|
import app.cash.molecule.RecompositionMode
|
||||||
|
import app.cash.molecule.moleculeFlow
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
|
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class UserStatusPresenterTest {
|
||||||
|
|
||||||
|
private fun createPresenter(client: FakeMatrixClient = FakeMatrixClient()) =
|
||||||
|
UserStatusPresenter(matrixClient = client)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial state - no status set`() = runTest {
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
createPresenter().present()
|
||||||
|
}.test {
|
||||||
|
val state = awaitItem()
|
||||||
|
assertThat(state.displayedStatus).isNull()
|
||||||
|
assertThat(state.pickerState).isEqualTo(UserStatusPickerState.Hidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Open event transitions to ShowingPicker`() = runTest {
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
createPresenter().present()
|
||||||
|
}.test {
|
||||||
|
val state = awaitItem()
|
||||||
|
state.eventSink(UserStatusEvent.Open)
|
||||||
|
assertThat(awaitItem().pickerState).isEqualTo(UserStatusPickerState.ShowingPicker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Dismiss event from ShowingPicker goes back to Hidden`() = runTest {
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
createPresenter().present()
|
||||||
|
}.test {
|
||||||
|
val state = awaitItem()
|
||||||
|
state.eventSink(UserStatusEvent.Open)
|
||||||
|
awaitItem() // ShowingPicker
|
||||||
|
state.eventSink(UserStatusEvent.Dismiss)
|
||||||
|
assertThat(awaitItem().pickerState).isEqualTo(UserStatusPickerState.Hidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Set event calls setUserStatus and transitions to Hidden with updated status`() = runTest {
|
||||||
|
val client = FakeMatrixClient()
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
createPresenter(client).present()
|
||||||
|
}.test {
|
||||||
|
val status = UserStatus(emoji = "💬", text = "In a meeting")
|
||||||
|
awaitItem().eventSink(UserStatusEvent.Set(status))
|
||||||
|
val newState = awaitItem()
|
||||||
|
assertThat(newState.pickerState).isEqualTo(UserStatusPickerState.Hidden)
|
||||||
|
assertThat(newState.displayedStatus).isEqualTo(DisplayedStatus.UserSet(status))
|
||||||
|
assertThat(client.setUserStatusCalled).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `OpenCustomInput with no existing status uses defaults`() = runTest {
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
createPresenter().present()
|
||||||
|
}.test {
|
||||||
|
awaitItem().eventSink(UserStatusEvent.OpenCustomInput)
|
||||||
|
val state = awaitItem()
|
||||||
|
val pickerState = state.pickerState as UserStatusPickerState.CustomInput
|
||||||
|
assertThat(pickerState.initialEmoji).isEqualTo("😀")
|
||||||
|
assertThat(pickerState.initialText).isEqualTo("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `OpenCustomInput with existing rawStatus pre-fills values`() = runTest {
|
||||||
|
val client = FakeMatrixClient()
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
createPresenter(client).present()
|
||||||
|
}.test {
|
||||||
|
val status = UserStatus(emoji = "🌴", text = "Away")
|
||||||
|
awaitItem().eventSink(UserStatusEvent.Set(status))
|
||||||
|
// Single emission: pickerState=Hidden and displayedStatus both set in one recomposition
|
||||||
|
awaitItem().eventSink(UserStatusEvent.OpenCustomInput)
|
||||||
|
val state = awaitItem()
|
||||||
|
val pickerState = state.pickerState as UserStatusPickerState.CustomInput
|
||||||
|
assertThat(pickerState.initialEmoji).isEqualTo("🌴")
|
||||||
|
assertThat(pickerState.initialText).isEqualTo("Away")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `CancelCustomInput goes back to Hidden`() = runTest {
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
createPresenter().present()
|
||||||
|
}.test {
|
||||||
|
awaitItem().eventSink(UserStatusEvent.OpenCustomInput)
|
||||||
|
awaitItem().eventSink(UserStatusEvent.CancelCustomInput) // CustomInput state, fire Cancel
|
||||||
|
assertThat(awaitItem().pickerState).isEqualTo(UserStatusPickerState.Hidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Clear event calls clearUserStatus and clears displayed status`() = runTest {
|
||||||
|
val client = FakeMatrixClient()
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
createPresenter(client).present()
|
||||||
|
}.test {
|
||||||
|
val status = UserStatus(emoji = "☕", text = "Be right back")
|
||||||
|
awaitItem().eventSink(UserStatusEvent.Set(status))
|
||||||
|
// Single emission: pickerState=Hidden and displayedStatus both set in one recomposition
|
||||||
|
awaitItem().eventSink(UserStatusEvent.Clear)
|
||||||
|
val clearedState = awaitItem()
|
||||||
|
assertThat(clearedState.pickerState).isEqualTo(UserStatusPickerState.Hidden)
|
||||||
|
assertThat(clearedState.displayedStatus).isNull()
|
||||||
|
assertThat(client.clearUserStatusCalled).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -258,7 +258,7 @@ class FakeMatrixClient(
|
|||||||
return removeAvatarResult
|
return removeAvatarResult
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setUserStatus(status: UserStatus): Result<Unit> = simulateLongTask {
|
override suspend fun setUserStatus(status: UserStatus): Result<Unit> {
|
||||||
setUserStatusCalled = true
|
setUserStatusCalled = true
|
||||||
if (setUserStatusResult.isSuccess) {
|
if (setUserStatusResult.isSuccess) {
|
||||||
_userProfile.emit(_userProfile.value.copy(
|
_userProfile.emit(_userProfile.value.copy(
|
||||||
@@ -269,7 +269,7 @@ class FakeMatrixClient(
|
|||||||
return setUserStatusResult
|
return setUserStatusResult
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun clearUserStatus(): Result<Unit> = simulateLongTask {
|
override suspend fun clearUserStatus(): Result<Unit> {
|
||||||
clearUserStatusCalled = true
|
clearUserStatusCalled = true
|
||||||
if (clearUserStatusResult.isSuccess) {
|
if (clearUserStatusResult.isSuccess) {
|
||||||
_userProfile.emit(_userProfile.value.copy(
|
_userProfile.emit(_userProfile.value.copy(
|
||||||
|
|||||||
Reference in New Issue
Block a user