Favorite : rework and add tests

This commit is contained in:
ganfra 2024-02-02 14:54:28 +01:00
parent a63d331f36
commit 7ca931c35f
21 changed files with 454 additions and 144 deletions

View file

@ -29,6 +29,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.Lifecycle
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
import io.element.android.features.roomactions.api.SetRoomIsFavoriteAction
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
@ -44,7 +45,6 @@ import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
import io.element.android.libraries.matrix.api.room.powerlevels.canSendState
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
import io.element.android.libraries.matrix.api.room.tags.RoomNotableTags
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
@ -61,6 +61,7 @@ class RoomDetailsPresenter @Inject constructor(
private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory,
private val leaveRoomPresenter: LeaveRoomPresenter,
private val dispatchers: CoroutineDispatchers,
private val setRoomIsFavorite: SetRoomIsFavoriteAction,
) : Presenter<RoomDetailsState> {
@Composable
override fun present(): RoomDetailsState {
@ -126,9 +127,8 @@ class RoomDetailsPresenter @Inject constructor(
}
}
is RoomDetailsEvent.SetIsFavorite -> {
scope.launch(dispatchers.io) {
val tags = RoomNotableTags(isFavorite = event.isFavorite)
room.updateNotableTags(tags)
scope.launch {
setRoomIsFavorite(room, event.isFavorite)
}
}
}

View file

@ -19,14 +19,18 @@ package io.element.android.features.roomdetails
import androidx.lifecycle.Lifecycle
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.TurbineTestContext
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.createroom.test.FakeStartDMAction
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter
import io.element.android.features.roomactions.api.SetRoomIsFavoriteAction
import io.element.android.features.roomactions.test.FakeSetRoomIsFavoriteAction
import io.element.android.features.roomdetails.impl.RoomDetailsEvent
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
import io.element.android.features.roomdetails.impl.RoomDetailsState
import io.element.android.features.roomdetails.impl.RoomDetailsType
import io.element.android.features.roomdetails.impl.RoomTopicState
import io.element.android.features.roomdetails.impl.members.aRoomMember
@ -40,6 +44,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.tags.RoomNotableTags
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_SESSION_ID
@ -71,10 +76,11 @@ class RoomDetailsPresenterTests {
}
private fun TestScope.createRoomDetailsPresenter(
room: MatrixRoom,
room: MatrixRoom = aMatrixRoom(),
leaveRoomPresenter: LeaveRoomPresenter = FakeLeaveRoomPresenter(),
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService()
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
setRoomIsFavoriteAction: SetRoomIsFavoriteAction = FakeSetRoomIsFavoriteAction(),
): RoomDetailsPresenter {
val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService)
val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory {
@ -86,25 +92,30 @@ class RoomDetailsPresenterTests {
mapOf(FeatureFlags.NotificationSettings.key to true)
)
return RoomDetailsPresenter(
matrixClient,
room,
featureFlagService,
matrixClient.notificationSettingsService(),
roomMemberDetailsPresenterFactory,
leaveRoomPresenter,
dispatchers
client = matrixClient,
room = room,
featureFlagService = featureFlagService,
notificationSettingsService = matrixClient.notificationSettingsService(),
roomMembersDetailsPresenterFactory = roomMemberDetailsPresenterFactory,
leaveRoomPresenter = leaveRoomPresenter,
dispatchers = dispatchers,
setRoomIsFavorite = setRoomIsFavoriteAction,
)
}
private suspend fun RoomDetailsPresenter.test(validate: suspend TurbineTestContext<RoomDetailsState>.() -> Unit) {
moleculeFlow(RecompositionMode.Immediate) {
withFakeLifecycleOwner(fakeLifecycleOwner) {
present()
}
}.test(validate = validate)
}
@Test
fun `present - initial state is created from room if roomInfo is null`() = runTest {
val room = aMatrixRoom()
val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test {
presenter.test {
val initialState = awaitItem()
assertThat(initialState.roomId).isEqualTo(room.roomId.value)
assertThat(initialState.roomName).isEqualTo(room.name)
@ -124,11 +135,7 @@ class RoomDetailsPresenterTests {
givenRoomInfo(roomInfo)
}
val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test {
presenter.test {
skipItems(1)
val updatedState = awaitItem()
assertThat(updatedState.roomName).isEqualTo(roomInfo.name)
@ -143,11 +150,7 @@ class RoomDetailsPresenterTests {
fun `present - initial state with no room name`() = runTest {
val room = aMatrixRoom(name = null)
val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test {
presenter.test {
val initialState = awaitItem()
assertThat(initialState.roomName).isEqualTo(room.displayName)
@ -167,11 +170,7 @@ class RoomDetailsPresenterTests {
givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
}
val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test {
presenter.test {
val initialState = awaitItem()
assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Dm(otherRoomMember))
@ -185,11 +184,7 @@ class RoomDetailsPresenterTests {
givenCanInviteResult(Result.success(true))
}
val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers())
moleculeFlow(RecompositionMode.Immediate) {
withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test {
presenter.test {
// Initially false
assertThat(awaitItem().canInvite).isFalse()
// Then the asynchronous check completes and it becomes true
@ -205,11 +200,7 @@ class RoomDetailsPresenterTests {
givenCanInviteResult(Result.success(false))
}
val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test {
presenter.test {
assertThat(awaitItem().canInvite).isFalse()
cancelAndIgnoreRemainingEvents()
@ -222,11 +213,7 @@ class RoomDetailsPresenterTests {
givenCanInviteResult(Result.failure(Throwable("Whoops")))
}
val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test {
presenter.test {
assertThat(awaitItem().canInvite).isFalse()
cancelAndIgnoreRemainingEvents()
@ -242,11 +229,7 @@ class RoomDetailsPresenterTests {
givenCanInviteResult(Result.success(false))
}
val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test {
presenter.test {
// Initially false
assertThat(awaitItem().canEdit).isFalse()
// Then the asynchronous check completes and it becomes true
@ -273,11 +256,7 @@ class RoomDetailsPresenterTests {
givenCanInviteResult(Result.success(false))
}
val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test {
presenter.test {
// Initially false
assertThat(awaitItem().canEdit).isFalse()
// Then the asynchronous check completes, but editing is still disallowed because it's a DM
@ -305,11 +284,7 @@ class RoomDetailsPresenterTests {
givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true))
}
val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test {
presenter.test {
skipItems(1)
// There's no topic, so we hide the entire UI for DMs
@ -328,11 +303,7 @@ class RoomDetailsPresenterTests {
givenCanInviteResult(Result.success(false))
}
val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test {
presenter.test {
// Initially false
assertThat(awaitItem().canEdit).isFalse()
// Then the asynchronous check completes and it becomes true
@ -351,11 +322,7 @@ class RoomDetailsPresenterTests {
givenCanInviteResult(Result.success(false))
}
val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test {
presenter.test {
// Initially false, and no further events
assertThat(awaitItem().canEdit).isFalse()
@ -371,11 +338,7 @@ class RoomDetailsPresenterTests {
}
val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test {
presenter.test {
// The initial state is "hidden" and no further state changes happen
assertThat(awaitItem().roomTopic).isEqualTo(RoomTopicState.Hidden)
@ -392,11 +355,7 @@ class RoomDetailsPresenterTests {
}
val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test {
presenter.test {
// Ignore the initial state
skipItems(1)
@ -416,11 +375,7 @@ class RoomDetailsPresenterTests {
leaveRoomPresenter = leaveRoomPresenter,
dispatchers = testCoroutineDispatchers()
)
moleculeFlow(RecompositionMode.Immediate) {
withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test {
presenter.test {
awaitItem().eventSink(RoomDetailsEvent.LeaveRoom)
assertThat(leaveRoomPresenter.events).contains(LeaveRoomEvent.ShowConfirmation(room.roomId))
@ -439,11 +394,7 @@ class RoomDetailsPresenterTests {
leaveRoomPresenter = leaveRoomPresenter,
notificationSettingsService = notificationSettingsService,
)
moleculeFlow(RecompositionMode.Immediate) {
withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test {
presenter.test {
notificationSettingsService.setRoomNotificationMode(room.roomId, RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
val updatedState = consumeItemsUntilPredicate {
it.roomNotificationSettings?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
@ -458,11 +409,7 @@ class RoomDetailsPresenterTests {
val notificationSettingsService = FakeNotificationSettingsService(initialRoomMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
val room = aMatrixRoom(notificationSettingsService = notificationSettingsService)
val presenter = createRoomDetailsPresenter(room = room, notificationSettingsService = notificationSettingsService)
moleculeFlow(RecompositionMode.Immediate) {
withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test {
presenter.test {
awaitItem().eventSink(RoomDetailsEvent.MuteNotification)
val updatedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) {
it.roomNotificationSettings?.mode == RoomNotificationMode.MUTE
@ -480,11 +427,7 @@ class RoomDetailsPresenterTests {
)
val room = aMatrixRoom(notificationSettingsService = notificationSettingsService)
val presenter = createRoomDetailsPresenter(room = room, notificationSettingsService = notificationSettingsService)
moleculeFlow(RecompositionMode.Immediate) {
withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test {
presenter.test {
awaitItem().eventSink(RoomDetailsEvent.UnmuteNotification)
val updatedState = consumeItemsUntilPredicate {
it.roomNotificationSettings?.mode == RoomNotificationMode.ALL_MESSAGES
@ -493,6 +436,35 @@ class RoomDetailsPresenterTests {
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - when set is favorite event is emitted, then the action is called`() = runTest {
val setRoomIsFavoriteAction = FakeSetRoomIsFavoriteAction()
val presenter = createRoomDetailsPresenter(setRoomIsFavoriteAction = setRoomIsFavoriteAction)
presenter.test {
val initialState = awaitItem()
initialState.eventSink(RoomDetailsEvent.SetIsFavorite(true))
setRoomIsFavoriteAction.assertCalled(1)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - changes in notable tags updates is favorite flag`() = runTest {
val room = aMatrixRoom()
val presenter = createRoomDetailsPresenter(room = room)
presenter.test {
room.updateNotableTags(RoomNotableTags(true))
consumeItemsUntilPredicate { it.isFavorite }.last().let { state ->
assertThat(state.isFavorite).isTrue()
}
room.updateNotableTags(RoomNotableTags(false))
consumeItemsUntilPredicate { !it.isFavorite }.last().let { state ->
assertThat(state.isFavorite).isFalse()
}
cancelAndIgnoreRemainingEvents()
}
}
}
fun aMatrixRoom(