Browse Source

Room navigation : fix tests on invite after the refactoring

pull/2695/head
ganfra 5 months ago
parent
commit
fbb92f0c9a
  1. 10
      appnav/src/test/kotlin/io/element/android/appnav/JoinRoomLoadedFlowNodeTest.kt
  2. 15
      features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt
  3. 4
      features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenter.kt
  4. 16
      features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt
  5. 8
      features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt
  6. 2
      features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteViewWrapper.kt
  7. 10
      features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InternalAcceptDeclineInviteEvents.kt
  8. 257
      features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenterTests.kt
  9. 248
      features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt
  10. 4
      features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt
  11. 4
      features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt
  12. 7
      features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt
  13. 4
      features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt
  14. 2
      features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt
  15. 1
      features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt
  16. 32
      features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt
  17. 1
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
  18. 12
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
  19. 11
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
  20. 3
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt
  21. 5
      libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt

10
appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt → appnav/src/test/kotlin/io/element/android/appnav/JoinRoomLoadedFlowNodeTest.kt

@ -32,7 +32,6 @@ import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
import io.element.android.libraries.architecture.childNode import io.element.android.libraries.architecture.childNode
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
@ -41,7 +40,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
class RoomFlowNodeTest { class JoinRoomLoadedFlowNodeTest {
@get:Rule @get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule() val instantTaskExecutorRule = InstantTaskExecutorRule()
@ -88,7 +87,7 @@ class RoomFlowNodeTest {
} }
} }
private fun aRoomFlowNode( private fun createJoinedRoomLoadedFlowNode(
plugins: List<Plugin>, plugins: List<Plugin>,
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(), messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(), roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
@ -99,7 +98,6 @@ class RoomFlowNodeTest {
messagesEntryPoint = messagesEntryPoint, messagesEntryPoint = messagesEntryPoint,
roomDetailsEntryPoint = roomDetailsEntryPoint, roomDetailsEntryPoint = roomDetailsEntryPoint,
appNavigationStateService = FakeAppNavigationStateService(), appNavigationStateService = FakeAppNavigationStateService(),
roomMembershipObserver = RoomMembershipObserver(),
appCoroutineScope = coroutineScope, appCoroutineScope = coroutineScope,
roomComponentFactory = FakeRoomComponentFactory(), roomComponentFactory = FakeRoomComponentFactory(),
matrixClient = FakeMatrixClient(), matrixClient = FakeMatrixClient(),
@ -111,7 +109,7 @@ class RoomFlowNodeTest {
val room = FakeMatrixRoom() val room = FakeMatrixRoom()
val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val inputs = JoinedRoomLoadedFlowNode.Inputs(room) val inputs = JoinedRoomLoadedFlowNode.Inputs(room)
val roomFlowNode = aRoomFlowNode( val roomFlowNode = createJoinedRoomLoadedFlowNode(
plugins = listOf(inputs), plugins = listOf(inputs),
messagesEntryPoint = fakeMessagesEntryPoint, messagesEntryPoint = fakeMessagesEntryPoint,
coroutineScope = this coroutineScope = this
@ -133,7 +131,7 @@ class RoomFlowNodeTest {
val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
val inputs = JoinedRoomLoadedFlowNode.Inputs(room) val inputs = JoinedRoomLoadedFlowNode.Inputs(room)
val roomFlowNode = aRoomFlowNode( val roomFlowNode = createJoinedRoomLoadedFlowNode(
plugins = listOf(inputs), plugins = listOf(inputs),
messagesEntryPoint = fakeMessagesEntryPoint, messagesEntryPoint = fakeMessagesEntryPoint,
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint, roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,

15
features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInvitePresenter.kt → features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt

@ -14,8 +14,19 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.invite.api.response package io.element.android.features.invite.impl.di
import com.squareup.anvil.annotations.ContributesTo
import dagger.Binds
import dagger.Module
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.SessionScope
interface AcceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> @ContributesTo(SessionScope::class)
@Module
interface InviteModule {
@Binds
fun bindAcceptDeclinePresenter(presenter: AcceptDeclineInvitePresenter): Presenter<AcceptDeclineInviteState>
}

4
features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenter.kt

@ -25,7 +25,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.InviteData import io.element.android.features.invite.api.response.InviteData
import io.element.android.features.invite.impl.model.InviteListInviteSummary import io.element.android.features.invite.impl.model.InviteListInviteSummary
import io.element.android.features.invite.impl.model.InviteSender import io.element.android.features.invite.impl.model.InviteSender
@ -42,7 +42,7 @@ import javax.inject.Inject
class InviteListPresenter @Inject constructor( class InviteListPresenter @Inject constructor(
private val client: MatrixClient, private val client: MatrixClient,
private val store: SeenInvitesStore, private val store: SeenInvitesStore,
private val acceptDeclineInvitePresenter: AcceptDeclineInvitePresenter, private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
) : Presenter<InviteListState> { ) : Presenter<InviteListState> {
@Composable @Composable
override fun present(): InviteListState { override fun present(): InviteListState {

16
features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInvitePresenter.kt → features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt

@ -26,10 +26,10 @@ import androidx.compose.runtime.setValue
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import im.vector.app.features.analytics.plan.JoinedRoom import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter
import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.InviteData import io.element.android.features.invite.api.response.InviteData
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.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ -44,12 +44,11 @@ import java.util.Optional
import javax.inject.Inject import javax.inject.Inject
import kotlin.jvm.optionals.getOrNull import kotlin.jvm.optionals.getOrNull
@ContributesBinding(SessionScope::class) class AcceptDeclineInvitePresenter @Inject constructor(
class DefaultAcceptDeclineInvitePresenter @Inject constructor(
private val client: MatrixClient, private val client: MatrixClient,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val notificationDrawerManager: NotificationDrawerManager, private val notificationDrawerManager: NotificationDrawerManager,
) : AcceptDeclineInvitePresenter { ) : Presenter<AcceptDeclineInviteState> {
@Composable @Composable
override fun present(): AcceptDeclineInviteState { override fun present(): AcceptDeclineInviteState {
@ -66,6 +65,7 @@ class DefaultAcceptDeclineInvitePresenter @Inject constructor(
is AcceptDeclineInviteEvents.AcceptInvite -> { is AcceptDeclineInviteEvents.AcceptInvite -> {
currentInvite = Optional.of(event.invite) currentInvite = Optional.of(event.invite)
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction) localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
currentInvite = Optional.empty()
} }
is AcceptDeclineInviteEvents.DeclineInvite -> { is AcceptDeclineInviteEvents.DeclineInvite -> {
@ -73,7 +73,7 @@ class DefaultAcceptDeclineInvitePresenter @Inject constructor(
declinedAction.value = AsyncAction.Confirming declinedAction.value = AsyncAction.Confirming
} }
is DefaultAcceptDeclineInviteEvents.ConfirmDeclineInvite -> { is InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite -> {
declinedAction.value = AsyncAction.Uninitialized declinedAction.value = AsyncAction.Uninitialized
currentInvite.getOrNull()?.let { currentInvite.getOrNull()?.let {
localCoroutineScope.declineInvite(it.roomId, declinedAction) localCoroutineScope.declineInvite(it.roomId, declinedAction)
@ -81,16 +81,16 @@ class DefaultAcceptDeclineInvitePresenter @Inject constructor(
currentInvite = Optional.empty() currentInvite = Optional.empty()
} }
is DefaultAcceptDeclineInviteEvents.CancelDeclineInvite -> { is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> {
currentInvite = Optional.empty() currentInvite = Optional.empty()
declinedAction.value = AsyncAction.Uninitialized declinedAction.value = AsyncAction.Uninitialized
} }
is DefaultAcceptDeclineInviteEvents.DismissAcceptError -> { is InternalAcceptDeclineInviteEvents.DismissAcceptError -> {
acceptedAction.value = AsyncAction.Uninitialized acceptedAction.value = AsyncAction.Uninitialized
} }
is DefaultAcceptDeclineInviteEvents.DismissDeclineError -> { is InternalAcceptDeclineInviteEvents.DismissDeclineError -> {
declinedAction.value = AsyncAction.Uninitialized declinedAction.value = AsyncAction.Uninitialized
} }
} }

8
features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt

@ -45,14 +45,14 @@ fun AcceptDeclineInviteView(
async = state.acceptAction, async = state.acceptAction,
onSuccess = onInviteAccepted, onSuccess = onInviteAccepted,
onErrorDismiss = { onErrorDismiss = {
state.eventSink(DefaultAcceptDeclineInviteEvents.DismissAcceptError) state.eventSink(InternalAcceptDeclineInviteEvents.DismissAcceptError)
}, },
) )
AsyncActionView( AsyncActionView(
async = state.declineAction, async = state.declineAction,
onSuccess = onInviteDeclined, onSuccess = onInviteDeclined,
onErrorDismiss = { onErrorDismiss = {
state.eventSink(DefaultAcceptDeclineInviteEvents.DismissDeclineError) state.eventSink(InternalAcceptDeclineInviteEvents.DismissDeclineError)
}, },
confirmationDialog = { confirmationDialog = {
val invite = state.invite.getOrNull() val invite = state.invite.getOrNull()
@ -60,10 +60,10 @@ fun AcceptDeclineInviteView(
DeclineConfirmationDialog( DeclineConfirmationDialog(
invite = invite, invite = invite,
onConfirmClicked = { onConfirmClicked = {
state.eventSink(DefaultAcceptDeclineInviteEvents.ConfirmDeclineInvite) state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite)
}, },
onDismissClicked = { onDismissClicked = {
state.eventSink(DefaultAcceptDeclineInviteEvents.CancelDeclineInvite) state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite)
} }
) )
} }

2
features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteView.kt → features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteViewWrapper.kt

@ -26,7 +26,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import javax.inject.Inject import javax.inject.Inject
@ContributesBinding(SessionScope::class) @ContributesBinding(SessionScope::class)
class DefaultAcceptDeclineInviteView @Inject constructor() : AcceptDeclineInviteView { class AcceptDeclineInviteViewWrapper @Inject constructor() : AcceptDeclineInviteView {
@Composable @Composable
override fun Render( override fun Render(

10
features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/DefaultAcceptDeclineInviteEvents.kt → features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InternalAcceptDeclineInviteEvents.kt

@ -18,9 +18,9 @@ package io.element.android.features.invite.impl.response
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
sealed interface DefaultAcceptDeclineInviteEvents: AcceptDeclineInviteEvents { sealed interface InternalAcceptDeclineInviteEvents: AcceptDeclineInviteEvents {
data object ConfirmDeclineInvite : DefaultAcceptDeclineInviteEvents data object ConfirmDeclineInvite : InternalAcceptDeclineInviteEvents
data object CancelDeclineInvite : DefaultAcceptDeclineInviteEvents data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents
data object DismissAcceptError : DefaultAcceptDeclineInviteEvents data object DismissAcceptError : InternalAcceptDeclineInviteEvents
data object DismissDeclineError : DefaultAcceptDeclineInviteEvents data object DismissDeclineError : InternalAcceptDeclineInviteEvents
} }

257
features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/InviteListPresenterTests.kt → features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenterTests.kt

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.invite.impl package io.element.android.features.invite.impl.invitelist
import app.cash.molecule.RecompositionMode import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
@ -22,11 +22,14 @@ import app.cash.turbine.TurbineTestContext
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
import io.element.android.features.invite.impl.invitelist.InviteListEvents import io.element.android.features.invite.impl.invitelist.InviteListEvents
import io.element.android.features.invite.impl.invitelist.InviteListPresenter import io.element.android.features.invite.impl.invitelist.InviteListPresenter
import io.element.android.features.invite.impl.invitelist.InviteListState import io.element.android.features.invite.impl.invitelist.InviteListState
import io.element.android.features.invite.test.FakeSeenInvitesStore import io.element.android.features.invite.test.FakeSeenInvitesStore
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@ -60,7 +63,7 @@ class InviteListPresenterTests {
@Test @Test
fun `present - starts empty, adds invites when received`() = runTest { fun `present - starts empty, adds invites when received`() = runTest {
val roomListService = FakeRoomListService() val roomListService = FakeRoomListService()
val presenter = createPresenter( val presenter = createInviteListPresenter(
FakeMatrixClient(roomListService = roomListService) FakeMatrixClient(roomListService = roomListService)
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
@ -81,7 +84,7 @@ class InviteListPresenterTests {
@Test @Test
fun `present - uses user ID and avatar for direct invites`() = runTest { fun `present - uses user ID and avatar for direct invites`() = runTest {
val roomListService = FakeRoomListService().withDirectChatInvitation() val roomListService = FakeRoomListService().withDirectChatInvitation()
val presenter = createPresenter( val presenter = createInviteListPresenter(
FakeMatrixClient(roomListService = roomListService) FakeMatrixClient(roomListService = roomListService)
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
@ -107,7 +110,7 @@ class InviteListPresenterTests {
@Test @Test
fun `present - includes sender details for room invites`() = runTest { fun `present - includes sender details for room invites`() = runTest {
val roomListService = FakeRoomListService().withRoomInvitation() val roomListService = FakeRoomListService().withRoomInvitation()
val presenter = createPresenter( val presenter = createInviteListPresenter(
FakeMatrixClient(roomListService = roomListService) FakeMatrixClient(roomListService = roomListService)
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
@ -128,247 +131,15 @@ class InviteListPresenterTests {
} }
} }
@Test
fun `present - shows confirm dialog for declining direct chat invites`() = runTest {
val roomListService = FakeRoomListService().withDirectChatInvitation()
val presenter = InviteListPresenter(
FakeMatrixClient(
roomListService = roomListService,
),
FakeSeenInvitesStore(),
FakeAnalyticsService(),
FakeNotificationDrawerManager()
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val originalState = awaitInitialItem()
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
val newState = awaitItem()
assertThat(newState.declineConfirmationDialog).isInstanceOf(InviteDeclineConfirmationDialog.Visible::class.java)
val confirmDialog = newState.declineConfirmationDialog as InviteDeclineConfirmationDialog.Visible
assertThat(confirmDialog.isDirect).isTrue()
assertThat(confirmDialog.name).isEqualTo(A_ROOM_NAME)
}
}
@Test
fun `present - shows confirm dialog for declining room invites`() = runTest {
val roomListService = FakeRoomListService().withRoomInvitation()
val presenter = createPresenter(
FakeMatrixClient(roomListService = roomListService)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val originalState = awaitInitialItem()
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
val newState = awaitItem()
assertThat(newState.declineConfirmationDialog).isInstanceOf(InviteDeclineConfirmationDialog.Visible::class.java)
val confirmDialog = newState.declineConfirmationDialog as InviteDeclineConfirmationDialog.Visible
assertThat(confirmDialog.isDirect).isFalse()
assertThat(confirmDialog.name).isEqualTo(A_ROOM_NAME)
}
}
@Test
fun `present - hides confirm dialog when cancelling`() = runTest {
val roomListService = FakeRoomListService().withRoomInvitation()
val presenter = createPresenter(
FakeMatrixClient(roomListService = roomListService)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val originalState = awaitInitialItem()
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
skipItems(1)
originalState.eventSink(InviteListEvents.CancelDeclineInvite)
val newState = awaitItem()
assertThat(newState.declineConfirmationDialog).isInstanceOf(InviteDeclineConfirmationDialog.Hidden::class.java)
}
}
@Test
fun `present - declines invite after confirming`() = runTest {
val roomListService = FakeRoomListService().withRoomInvitation()
val fakeNotificationDrawerManager = FakeNotificationDrawerManager()
val client = FakeMatrixClient(
roomListService = roomListService,
)
val room = FakeMatrixRoom()
val presenter = createPresenter(client = client, notificationDrawerManager = fakeNotificationDrawerManager)
client.givenGetRoomResult(A_ROOM_ID, room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val originalState = awaitInitialItem()
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
skipItems(1)
originalState.eventSink(InviteListEvents.ConfirmDeclineInvite)
skipItems(2)
assertThat(fakeNotificationDrawerManager.getClearMembershipNotificationForRoomCount(client.sessionId, A_ROOM_ID)).isEqualTo(1)
}
}
@Test
fun `present - declines invite after confirming and sets state on error`() = runTest {
val roomListService = FakeRoomListService().withRoomInvitation()
val client = FakeMatrixClient(
roomListService = roomListService,
)
val room = FakeMatrixRoom()
val presenter = createPresenter(client)
val ex = Throwable("Ruh roh!")
room.givenLeaveRoomError(ex)
client.givenGetRoomResult(A_ROOM_ID, room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val originalState = awaitInitialItem()
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
skipItems(1)
originalState.eventSink(InviteListEvents.ConfirmDeclineInvite)
skipItems(1)
val newState = awaitItem()
assertThat(newState.declinedAction).isEqualTo(AsyncData.Failure<Unit>(ex))
}
}
@Test
fun `present - dismisses declining error state`() = runTest {
val roomListService = FakeRoomListService().withRoomInvitation()
val client = FakeMatrixClient(
roomListService = roomListService,
)
val room = FakeMatrixRoom()
val presenter = createPresenter(client)
val ex = Throwable("Ruh roh!")
room.givenLeaveRoomError(ex)
client.givenGetRoomResult(A_ROOM_ID, room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val originalState = awaitInitialItem()
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
skipItems(1)
originalState.eventSink(InviteListEvents.ConfirmDeclineInvite)
skipItems(2)
originalState.eventSink(InviteListEvents.DismissDeclineError)
val newState = awaitItem()
assertThat(newState.declinedAction).isEqualTo(AsyncData.Uninitialized)
}
}
@Test
fun `present - accepts invites and sets state on success`() = runTest {
val roomListService = FakeRoomListService().withRoomInvitation()
val fakeNotificationDrawerManager = FakeNotificationDrawerManager()
val client = FakeMatrixClient(
roomListService = roomListService,
)
val room = FakeMatrixRoom()
val presenter = createPresenter(client = client, notificationDrawerManager = fakeNotificationDrawerManager)
client.givenGetRoomResult(A_ROOM_ID, room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val originalState = awaitInitialItem()
originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0]))
val newState = awaitItem()
assertThat(newState.acceptedAction).isEqualTo(AsyncData.Success(A_ROOM_ID))
assertThat(fakeNotificationDrawerManager.getClearMembershipNotificationForRoomCount(client.sessionId, A_ROOM_ID)).isEqualTo(1)
}
}
@Test
fun `present - accepts invites and sets state on error`() = runTest {
val roomListService = FakeRoomListService().withRoomInvitation()
val client = FakeMatrixClient(
roomListService = roomListService,
)
val room = FakeMatrixRoom()
val presenter = createPresenter(client)
val ex = Throwable("Ruh roh!")
room.givenJoinRoomResult(Result.failure(ex))
client.givenGetRoomResult(A_ROOM_ID, room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val originalState = awaitInitialItem()
originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0]))
assertThat(awaitItem().acceptedAction).isEqualTo(AsyncData.Failure<RoomId>(ex))
}
}
@Test
fun `present - dismisses accepting error state`() = runTest {
val roomListService = FakeRoomListService().withRoomInvitation()
val client = FakeMatrixClient(
roomListService = roomListService,
)
val room = FakeMatrixRoom()
val presenter = createPresenter(client)
val ex = Throwable("Ruh roh!")
room.givenJoinRoomResult(Result.failure(ex))
client.givenGetRoomResult(A_ROOM_ID, room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val originalState = awaitInitialItem()
originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0]))
skipItems(1)
originalState.eventSink(InviteListEvents.DismissAcceptError)
val newState = awaitItem()
assertThat(newState.acceptedAction).isEqualTo(AsyncData.Uninitialized)
}
}
@Test @Test
fun `present - stores seen invites when received`() = runTest { fun `present - stores seen invites when received`() = runTest {
val roomListService = FakeRoomListService() val roomListService = FakeRoomListService()
val store = FakeSeenInvitesStore() val store = FakeSeenInvitesStore()
val presenter = InviteListPresenter( val presenter = createInviteListPresenter(
FakeMatrixClient( FakeMatrixClient(
roomListService = roomListService, roomListService = roomListService,
), ),
store, store,
FakeAnalyticsService(),
FakeNotificationDrawerManager()
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -400,13 +171,11 @@ class InviteListPresenterTests {
val roomListService = FakeRoomListService() val roomListService = FakeRoomListService()
val store = FakeSeenInvitesStore() val store = FakeSeenInvitesStore()
store.publishRoomIds(setOf(A_ROOM_ID)) store.publishRoomIds(setOf(A_ROOM_ID))
val presenter = InviteListPresenter( val presenter = createInviteListPresenter(
FakeMatrixClient( FakeMatrixClient(
roomListService = roomListService, roomListService = roomListService,
), ),
store, store,
FakeAnalyticsService(),
FakeNotificationDrawerManager()
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -494,15 +263,13 @@ class InviteListPresenterTests {
return awaitItem() return awaitItem()
} }
private fun createPresenter( private fun createInviteListPresenter(
client: MatrixClient, client: MatrixClient,
seenInvitesStore: SeenInvitesStore = FakeSeenInvitesStore(), seenInvitesStore: SeenInvitesStore = FakeSeenInvitesStore(),
fakeAnalyticsService: AnalyticsService = FakeAnalyticsService(), acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
notificationDrawerManager: NotificationDrawerManager = FakeNotificationDrawerManager()
) = InviteListPresenter( ) = InviteListPresenter(
client, client,
seenInvitesStore, seenInvitesStore,
fakeAnalyticsService, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
notificationDrawerManager
) )
} }

248
features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt

@ -0,0 +1,248 @@
/*
* 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.invite.impl.response
import com.google.common.truth.Truth.assertThat
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.InviteData
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
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.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import java.util.Optional
class AcceptDeclineInvitePresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
val presenter = createAcceptDeclineInvitePresenter()
presenter.test {
awaitItem().also { state ->
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
}
}
}
@Test
fun `present - declining invite cancel flow`() = runTest {
val presenter = createAcceptDeclineInvitePresenter()
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
)
}
skipItems(1)
awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.of(inviteData))
assertThat(state.declineAction).isInstanceOf(AsyncAction.Confirming::class.java)
state.eventSink(
InternalAcceptDeclineInviteEvents.CancelDeclineInvite
)
}
skipItems(1)
awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
}
}
}
@Test
fun `present - declining invite error flow`() = runTest {
val declineInviteFailure = lambdaRecorder { ->
Result.failure<Unit>(RuntimeException("Failed to leave room"))
}
val client = FakeMatrixClient().apply {
givenGetRoomResult(
roomId = A_ROOM_ID,
result = FakeMatrixRoom().apply {
leaveRoomLambda = declineInviteFailure
}
)
}
val presenter = createAcceptDeclineInvitePresenter(client = client)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
)
}
skipItems(1)
awaitItem().also { state ->
state.eventSink(
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
)
}
skipItems(1)
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(
InternalAcceptDeclineInviteEvents.DismissDeclineError
)
}
skipItems(1)
awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
}
cancelAndConsumeRemainingEvents()
}
assert(declineInviteFailure).isCalledOnce()
}
@Test
fun `present - declining invite success flow`() = runTest {
val declineInviteSuccess = lambdaRecorder { ->
Result.success(Unit)
}
val client = FakeMatrixClient().apply {
givenGetRoomResult(
roomId = A_ROOM_ID,
result = FakeMatrixRoom().apply {
leaveRoomLambda = declineInviteSuccess
}
)
}
val presenter = createAcceptDeclineInvitePresenter(client = client)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
)
}
skipItems(1)
awaitItem().also { state ->
state.eventSink(
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
)
}
skipItems(1)
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java)
}
cancelAndConsumeRemainingEvents()
}
assert(declineInviteSuccess).isCalledOnce()
}
@Test
fun `present - accepting invite error flow`() = runTest {
val joinRoomFailure = lambdaRecorder { roomId: RoomId ->
Result.failure<RoomId>(RuntimeException("Failed to join room $roomId"))
}
val client = FakeMatrixClient().apply {
joinRoomLambda = joinRoomFailure
}
val presenter = createAcceptDeclineInvitePresenter(client = client)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
)
}
skipItems(1)
awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.of(inviteData))
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(
InternalAcceptDeclineInviteEvents.DismissAcceptError
)
}
skipItems(1)
awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
}
cancelAndConsumeRemainingEvents()
}
assert(joinRoomFailure).isCalledOnce()
}
@Test
fun `present - accepting invite success flow`() = runTest {
val joinRoomSuccess = lambdaRecorder { roomId: RoomId ->
Result.success(roomId)
}
val client = FakeMatrixClient().apply {
joinRoomLambda = joinRoomSuccess
}
val presenter = createAcceptDeclineInvitePresenter(client = client)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
)
}
skipItems(1)
awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.of(inviteData))
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Success::class.java)
}
cancelAndConsumeRemainingEvents()
}
assert(joinRoomSuccess).isCalledOnce()
}
private fun anInviteData(
roomId: RoomId = A_ROOM_ID,
name: String = A_ROOM_NAME,
isDirect: Boolean = false
): InviteData {
return InviteData(
roomId = roomId,
roomName = name,
isDirect = isDirect
)
}
private fun createAcceptDeclineInvitePresenter(
client: MatrixClient = FakeMatrixClient(),
analyticsService: AnalyticsService = FakeAnalyticsService(),
notificationDrawerManager: NotificationDrawerManager = FakeNotificationDrawerManager(),
): AcceptDeclineInvitePresenter {
return AcceptDeclineInvitePresenter(
client = client,
analyticsService = analyticsService,
notificationDrawerManager = notificationDrawerManager,
)
}
}

4
features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt

@ -23,7 +23,7 @@ import androidx.compose.runtime.produceState
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.InviteData import io.element.android.features.invite.api.response.InviteData
import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@ -39,7 +39,7 @@ class JoinRoomPresenter @AssistedInject constructor(
@Assisted private val roomId: RoomId, @Assisted private val roomId: RoomId,
@Assisted private val roomDescription: Optional<RoomDescription>, @Assisted private val roomDescription: Optional<RoomDescription>,
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val acceptDeclineInvitePresenter: AcceptDeclineInvitePresenter, private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
) : Presenter<JoinRoomState> { ) : Presenter<JoinRoomState> {
interface Factory { interface Factory {

4
features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt

@ -187,7 +187,7 @@ private fun JoinRoomContent(
} }
@Composable @Composable
fun JoinRoomMembersCount(memberCount: Long) { private fun JoinRoomMembersCount(memberCount: Long) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Row( Row(
modifier = Modifier modifier = Modifier
@ -227,7 +227,7 @@ private fun JoinRoomTopBar(
@PreviewLightDark @PreviewLightDark
@Composable @Composable
fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview { internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview {
JoinRoomView( JoinRoomView(
state = state, state = state,
onBackPressed = { } onBackPressed = { }

7
features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt

@ -19,13 +19,13 @@ package io.element.android.features.joinroom.impl.di
import com.squareup.anvil.annotations.ContributesTo import com.squareup.anvil.annotations.ContributesTo
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.joinroom.impl.JoinRoomPresenter import io.element.android.features.joinroom.impl.JoinRoomPresenter
import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
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.roomlist.RoomListService
import java.util.Optional import java.util.Optional
@Module @Module
@ -33,9 +33,8 @@ import java.util.Optional
object JoinRoomModule { object JoinRoomModule {
@Provides @Provides
fun providesJoinRoomPresenterFactory( fun providesJoinRoomPresenterFactory(
roomListService: RoomListService,
client: MatrixClient, client: MatrixClient,
acceptDeclineInvitePresenter: AcceptDeclineInvitePresenter, acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
): JoinRoomPresenter.Factory { ): JoinRoomPresenter.Factory {
return object : JoinRoomPresenter.Factory { return object : JoinRoomPresenter.Factory {
override fun create(roomId: RoomId, roomDescription: Optional<RoomDescription>): JoinRoomPresenter { override fun create(roomId: RoomId, roomDescription: Optional<RoomDescription>): JoinRoomPresenter {

4
features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt

@ -164,7 +164,7 @@ class LeaveRoomPresenterImplTest {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
result = FakeMatrixRoom().apply { result = FakeMatrixRoom().apply {
givenLeaveRoomError(RuntimeException("Blimey!")) this.leaveRoomLambda = { Result.failure(RuntimeException("Blimey!"))}
}, },
) )
} }
@ -210,7 +210,7 @@ class LeaveRoomPresenterImplTest {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
result = FakeMatrixRoom().apply { result = FakeMatrixRoom().apply {
givenLeaveRoomError(RuntimeException("Blimey!")) this.leaveRoomLambda = { Result.failure(RuntimeException("Blimey!"))}
}, },
) )
} }

2
features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt

@ -18,9 +18,11 @@ package io.element.android.features.location.impl.common.actions
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.location.api.Location import io.element.android.features.location.api.Location
import org.junit.Ignore
import org.junit.Test import org.junit.Test
import java.net.URLEncoder import java.net.URLEncoder
@Ignore
internal class AndroidLocationActionsTest { internal class AndroidLocationActionsTest {
// We use an Android-native encoder in the actual app, switch to an equivalent JVM one for the tests // We use an Android-native encoder in the actual app, switch to an equivalent JVM one for the tests
private fun urlEncoder(input: String) = URLEncoder.encode(input, "US-ASCII") private fun urlEncoder(input: String) = URLEncoder.encode(input, "US-ASCII")

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

@ -314,6 +314,7 @@ private fun RoomDirectoryRoomRow(
.align(Alignment.CenterVertically) .align(Alignment.CenterVertically)
.clickable(onClick = onJoinClick) .clickable(onClick = onJoinClick)
.padding(start = 4.dp, end = 12.dp) .padding(start = 4.dp, end = 12.dp)
.testTag(TestTags.callToAction.value)
) )
} else { } else {
Spacer(modifier = Modifier.width(24.dp)) Spacer(modifier = Modifier.width(24.dp))

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

@ -17,16 +17,23 @@
package io.element.android.features.roomdirectory.impl.root package io.element.android.features.roomdirectory.impl.root
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onChild
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
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.testtags.TestTags
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
@ -55,15 +62,32 @@ class RoomDirectoryViewTest {
} }
@Test @Test
fun `clicking on room item emits the expected Event`() { fun `clicking on room item then onResultClicked lambda is called once`() {
val eventsRecorder = EventsRecorder<RoomDirectoryEvents>() val eventsRecorder = EventsRecorder<RoomDirectoryEvents>()
val state = aRoomDirectoryState( val state = aRoomDirectoryState(
roomDescriptions = aRoomDescriptionList(), roomDescriptions = aRoomDescriptionList(),
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
rule.setRoomDirectoryView(state = state)
val clickedRoom = state.roomDescriptions.first() val clickedRoom = state.roomDescriptions.first()
ensureCalledOnceWithParam(clickedRoom) { callback ->
rule.setRoomDirectoryView(
state = state,
onResultClicked = callback,
)
rule.onNodeWithText(clickedRoom.name).performClick() rule.onNodeWithText(clickedRoom.name).performClick()
}
}
@Test
fun `clicking on room item join cta 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.onAllNodesWithTag(TestTags.callToAction.value).onFirst().performClick()
eventsRecorder.assertSingle(RoomDirectoryEvents.JoinRoom(clickedRoom.roomId)) eventsRecorder.assertSingle(RoomDirectoryEvents.JoinRoom(clickedRoom.roomId))
} }
@ -100,12 +124,14 @@ class RoomDirectoryViewTest {
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomDirectoryView( private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomDirectoryView(
state: RoomDirectoryState, state: RoomDirectoryState,
onBackPressed: () -> Unit = EnsureNeverCalled(), onBackPressed: () -> Unit = EnsureNeverCalled(),
onResultClicked: (RoomDescription) -> Unit = EnsureNeverCalledWithParam(),
onRoomJoined: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onRoomJoined: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
) { ) {
setContent { setContent {
RoomDirectoryView( RoomDirectoryView(
state = state, state = state,
onRoomClicked = onRoomJoined, onResultClicked = onResultClicked,
onRoomJoined = onRoomJoined,
onBackPressed = onBackPressed, onBackPressed = onBackPressed,
) )
} }

1
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt

@ -100,3 +100,4 @@ interface MatrixClient : Closeable {
suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result<Unit> suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result<Unit>
suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>> suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>>
} }

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

@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.notificationsettings.Notification
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
@ -51,7 +52,9 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import java.util.Optional
class FakeMatrixClient( class FakeMatrixClient(
override val sessionId: SessionId = A_SESSION_ID, override val sessionId: SessionId = A_SESSION_ID,
@ -94,10 +97,14 @@ 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> = { var joinRoomLambda: (RoomId) -> Result<RoomId> = {
Result.success(it) Result.success(it)
} }
var getRoomInfoFlowLambda = { _: RoomId ->
flowOf<Optional<MatrixRoomInfo>>(Optional.empty())
}
override suspend fun getRoom(roomId: RoomId): MatrixRoom? { override suspend fun getRoom(roomId: RoomId): MatrixRoom? {
return getRoomResults[roomId] return getRoomResults[roomId]
} }
@ -267,4 +274,7 @@ class FakeMatrixClient(
override suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>> { override suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>> {
return Result.success(visitedRoomsId) return Result.success(visitedRoomsId)
} }
override fun getRoomInfoFlow(roomId: RoomId) = getRoomInfoFlowLambda(roomId)
} }

11
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt

@ -182,7 +182,7 @@ class FakeMatrixRoom(
var removedAvatar: Boolean = false var removedAvatar: Boolean = false
private set private set
private var leaveRoomError: Throwable? = null var leaveRoomLambda: (() -> Result<Unit>) = { Result.success(Unit) }
private val _roomInfoFlow: MutableSharedFlow<MatrixRoomInfo> = MutableSharedFlow(replay = 1) private val _roomInfoFlow: MutableSharedFlow<MatrixRoomInfo> = MutableSharedFlow(replay = 1)
override val roomInfoFlow: Flow<MatrixRoomInfo> = _roomInfoFlow override val roomInfoFlow: Flow<MatrixRoomInfo> = _roomInfoFlow
@ -315,8 +315,9 @@ class FakeMatrixRoom(
return Result.success(Unit) return Result.success(Unit)
} }
override suspend fun leave(): Result<Unit> = override suspend fun leave(): Result<Unit> {
leaveRoomError?.let { Result.failure(it) } ?: Result.success(Unit) return leaveRoomLambda()
}
override suspend fun join(): Result<Unit> { override suspend fun join(): Result<Unit> {
return joinRoomResult return joinRoomResult
@ -542,10 +543,6 @@ class FakeMatrixRoom(
override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver> = getWidgetDriverResult override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver> = getWidgetDriverResult
fun givenLeaveRoomError(throwable: Throwable?) {
this.leaveRoomError = throwable
}
fun givenRoomMembersState(state: MatrixRoomMembersState) { fun givenRoomMembersState(state: MatrixRoomMembersState) {
membersStateFlow.value = state membersStateFlow.value = state
} }

3
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt

@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.roomlist.RoomList
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.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.getAndUpdate
@ -32,6 +33,8 @@ data class SimplePagedRoomList(
override val pageSize: Int = Int.MAX_VALUE override val pageSize: Int = Int.MAX_VALUE
override val loadedPages = MutableStateFlow(1) override val loadedPages = MutableStateFlow(1)
override val filteredSummaries: SharedFlow<List<RoomSummary>> = summaries
override suspend fun loadMore() { override suspend fun loadMore() {
// No-op // No-op
loadedPages.getAndUpdate { it + 1 } loadedPages.getAndUpdate { it + 1 }

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

@ -105,4 +105,9 @@ object TestTags {
* Search field. * Search field.
*/ */
val searchTextField = TestTag("search_text_field") val searchTextField = TestTag("search_text_field")
/**
* Generic call to action
*/
val callToAction = TestTag("call_to_action")
} }

Loading…
Cancel
Save