knock requests : add tests to the feature
This commit is contained in:
parent
108253ca82
commit
a6decdf697
8 changed files with 886 additions and 17 deletions
|
|
@ -14,6 +14,11 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.knockrequests.impl"
|
namespace = "io.element.android.features.knockrequests.impl"
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupAnvil()
|
setupAnvil()
|
||||||
|
|
@ -31,7 +36,12 @@ dependencies {
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
testImplementation(libs.molecule.runtime)
|
testImplementation(libs.molecule.runtime)
|
||||||
|
testImplementation(libs.test.robolectric)
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.tests.testutils)
|
||||||
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.knockrequests.impl.data
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@ContributesTo(RoomScope::class)
|
||||||
|
object KnockRequestsModule {
|
||||||
|
@Provides
|
||||||
|
@SingleIn(RoomScope::class)
|
||||||
|
fun knockRequestsService(room: MatrixRoom): KnockRequestsService {
|
||||||
|
return KnockRequestsService(room.knockRequestsFlow, room.roomCoroutineScope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,13 +8,13 @@
|
||||||
package io.element.android.features.knockrequests.impl.data
|
package io.element.android.features.knockrequests.impl.data
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.di.RoomScope
|
|
||||||
import io.element.android.libraries.di.SingleIn
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
|
@ -22,23 +22,24 @@ import kotlinx.coroutines.flow.getAndUpdate
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@SingleIn(RoomScope::class)
|
class KnockRequestsService(
|
||||||
class KnockRequestsService @Inject constructor(room: MatrixRoom) {
|
knockRequestsFlow: Flow<List<KnockRequest>>,
|
||||||
|
coroutineScope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
|
||||||
// Keep track of the knock requests that have been handled, so we don't have to wait for sync to remove them.
|
// Keep track of the knock requests that have been handled, so we don't have to wait for sync to remove them.
|
||||||
private val handledKnockRequestIds = MutableStateFlow<Set<EventId>>(emptySet())
|
private val handledKnockRequestIds = MutableStateFlow<Set<EventId>>(emptySet())
|
||||||
|
|
||||||
val knockRequestsFlow = combine(
|
val knockRequestsFlow = combine(
|
||||||
room.wrappedKnockRequestsFlow(),
|
knockRequestsFlow.wrapped(),
|
||||||
handledKnockRequestIds,
|
handledKnockRequestIds,
|
||||||
) { knockRequests, handledKnockIds ->
|
) { knockRequests, handledKnockIds ->
|
||||||
val presentableKnockRequests = knockRequests
|
val presentableKnockRequests = knockRequests
|
||||||
.filter { it.eventId !in handledKnockIds }
|
.filter { it.eventId !in handledKnockIds }
|
||||||
.toImmutableList()
|
.toImmutableList()
|
||||||
AsyncData.Success(presentableKnockRequests)
|
AsyncData.Success(presentableKnockRequests)
|
||||||
}.stateIn(room.roomCoroutineScope, SharingStarted.Lazily, AsyncData.Loading())
|
}.stateIn(coroutineScope, SharingStarted.Lazily, AsyncData.Loading())
|
||||||
|
|
||||||
private fun knockRequestsList() = knockRequestsFlow.value.dataOrNull().orEmpty()
|
private fun knockRequestsList() = knockRequestsFlow.value.dataOrNull().orEmpty()
|
||||||
|
|
||||||
|
|
@ -129,7 +130,7 @@ class KnockRequestsService @Inject constructor(room: MatrixRoom) {
|
||||||
|
|
||||||
private fun knockRequestNotFoundResult() = Result.failure<Unit>(IllegalArgumentException("Knock request not found"))
|
private fun knockRequestNotFoundResult() = Result.failure<Unit>(IllegalArgumentException("Knock request not found"))
|
||||||
|
|
||||||
private fun MatrixRoom.wrappedKnockRequestsFlow() = knockRequestsFlow.map { knockRequests ->
|
private fun Flow<List<KnockRequest>>.wrapped() = map { knockRequests ->
|
||||||
knockRequests.map { KnockRequestWrapper(it) }
|
knockRequests.map { KnockRequestWrapper(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,253 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.knockrequests.impl.banner
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.knockrequests.impl.data.KnockRequestsService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
|
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.test.room.knock.FakeKnockRequest
|
||||||
|
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.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class) class KnockRequestsBannerPresenterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when feature is disabled then the banner should be hidden`() = runTest {
|
||||||
|
val knockRequests = flowOf(listOf(FakeKnockRequest()))
|
||||||
|
val presenter = createKnockRequestsBannerPresenter(isFeatureEnabled = false, knockRequestsFlow = knockRequests)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.isVisible).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when empty knock request list then the banner should be hidden`() = runTest {
|
||||||
|
val knockRequests = flowOf(emptyList<KnockRequest>())
|
||||||
|
val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.isVisible).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when no permission to manage knock requests then the banner should be hidden`() = runTest {
|
||||||
|
val presenter = createKnockRequestsBannerPresenter(canAcceptKnockRequests = false)
|
||||||
|
presenter.test {
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.isVisible).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when everything is setup to manage knocks with data, then the banner should be visible `() = runTest {
|
||||||
|
val knockRequests = flowOf(
|
||||||
|
listOf(
|
||||||
|
FakeKnockRequest(
|
||||||
|
reason = "A reason",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.isVisible).isTrue()
|
||||||
|
assertThat(state.knockRequests).hasSize(1)
|
||||||
|
assertThat(state.canAccept).isTrue()
|
||||||
|
assertThat(state.reason).isEqualTo("A reason")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when multiple knock requests, the banner should not have reason nor subtitle`() = runTest {
|
||||||
|
val knockRequests = flowOf(
|
||||||
|
listOf(
|
||||||
|
FakeKnockRequest(
|
||||||
|
displayName = "Alice",
|
||||||
|
),
|
||||||
|
FakeKnockRequest(
|
||||||
|
displayName = "Bob",
|
||||||
|
),
|
||||||
|
FakeKnockRequest(
|
||||||
|
displayName = "Charlie",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.isVisible).isTrue()
|
||||||
|
assertThat(state.knockRequests).hasSize(3)
|
||||||
|
assertThat(state.reason).isNull()
|
||||||
|
assertThat(state.subtitle).isNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when there are some seen knock requests, then the banner should filtered them`() = runTest {
|
||||||
|
val knockRequests = flowOf(
|
||||||
|
listOf(
|
||||||
|
FakeKnockRequest(
|
||||||
|
displayName = "Alice",
|
||||||
|
isSeen = true,
|
||||||
|
userId = A_USER_ID
|
||||||
|
),
|
||||||
|
FakeKnockRequest(
|
||||||
|
displayName = "Bob",
|
||||||
|
isSeen = true,
|
||||||
|
userId = A_USER_ID_2
|
||||||
|
),
|
||||||
|
FakeKnockRequest(
|
||||||
|
isSeen = false,
|
||||||
|
displayName = "Charlie",
|
||||||
|
reason = "A reason",
|
||||||
|
userId = A_USER_ID_3
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.isVisible).isTrue()
|
||||||
|
// Only Charlie should be displayed
|
||||||
|
assertThat(state.knockRequests).hasSize(1)
|
||||||
|
assertThat(state.reason).isEqualTo("A reason")
|
||||||
|
assertThat(state.subtitle).isEqualTo(A_USER_ID_3.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - given AcceptSingleRequest event with failure, then the banner should hide and reappear and error should appear and disappear`() = runTest {
|
||||||
|
val acceptLambda = lambdaRecorder<Result<Unit>> { Result.failure(Exception()) }
|
||||||
|
val knockRequest = FakeKnockRequest(
|
||||||
|
displayName = "Alice",
|
||||||
|
reason = "A reason",
|
||||||
|
acceptLambda = acceptLambda
|
||||||
|
)
|
||||||
|
val knockRequests = flowOf(listOf(knockRequest))
|
||||||
|
val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
state.eventSink(KnockRequestsBannerEvents.AcceptSingleRequest)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.isVisible).isFalse()
|
||||||
|
assertThat(state.displayAcceptError).isFalse()
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.isVisible).isFalse()
|
||||||
|
assertThat(state.displayAcceptError).isTrue()
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.isVisible).isTrue()
|
||||||
|
assertThat(state.displayAcceptError).isTrue()
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.isVisible).isTrue()
|
||||||
|
assertThat(state.displayAcceptError).isFalse()
|
||||||
|
}
|
||||||
|
assert(acceptLambda).isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - given an AcceptSingleRequest event with success, then banner should be dismissed`() = runTest {
|
||||||
|
val acceptLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||||
|
val knockRequest = FakeKnockRequest(
|
||||||
|
displayName = "Alice",
|
||||||
|
reason = "A reason",
|
||||||
|
acceptLambda = acceptLambda
|
||||||
|
)
|
||||||
|
val knockRequests = flowOf(listOf(knockRequest))
|
||||||
|
val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.knockRequests).hasSize(1)
|
||||||
|
state.eventSink(KnockRequestsBannerEvents.AcceptSingleRequest)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.isVisible).isFalse()
|
||||||
|
}
|
||||||
|
advanceUntilIdle()
|
||||||
|
assert(acceptLambda).isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - given a Dismiss event, then knock requests should be marked as seen`() = runTest {
|
||||||
|
val markAsSeenLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||||
|
val knockRequests = flowOf(
|
||||||
|
listOf(
|
||||||
|
FakeKnockRequest(markAsSeenLambda = markAsSeenLambda),
|
||||||
|
FakeKnockRequest(markAsSeenLambda = markAsSeenLambda),
|
||||||
|
FakeKnockRequest(markAsSeenLambda = markAsSeenLambda),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
state.eventSink(KnockRequestsBannerEvents.Dismiss)
|
||||||
|
}
|
||||||
|
advanceUntilIdle()
|
||||||
|
assert(markAsSeenLambda).isCalledExactly(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createKnockRequestsBannerPresenter(
|
||||||
|
knockRequestsFlow: Flow<List<KnockRequest>> = flowOf(emptyList()),
|
||||||
|
canAcceptKnockRequests: Boolean = true,
|
||||||
|
isFeatureEnabled: Boolean = true,
|
||||||
|
): KnockRequestsBannerPresenter {
|
||||||
|
val knockRequestsService = KnockRequestsService(
|
||||||
|
knockRequestsFlow = knockRequestsFlow,
|
||||||
|
coroutineScope = backgroundScope
|
||||||
|
)
|
||||||
|
val featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(
|
||||||
|
FeatureFlags.Knock.key to isFeatureEnabled
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return KnockRequestsBannerPresenter(
|
||||||
|
room = FakeMatrixRoom(
|
||||||
|
canInviteResult = { Result.success(canAcceptKnockRequests) }
|
||||||
|
),
|
||||||
|
knockRequestsService = knockRequestsService,
|
||||||
|
appCoroutineScope = this,
|
||||||
|
featureFlagService = featureFlagService
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.knockrequests.impl.banner
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.features.knockrequests.impl.R
|
||||||
|
import io.element.android.features.knockrequests.impl.data.aKnockRequest
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class KnockRequestsListViewTest {
|
||||||
|
@get:Rule
|
||||||
|
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on view on single request invoke the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<KnockRequestsBannerEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setKnockRequestsBannerView(
|
||||||
|
state = aKnockRequestsBannerState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onViewRequestsClick = it
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.screen_room_single_knock_request_view_button_title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on view all when multiple requests invoke the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<KnockRequestsBannerEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setKnockRequestsBannerView(
|
||||||
|
state = aKnockRequestsBannerState(
|
||||||
|
knockRequests = listOf(
|
||||||
|
aKnockRequest(displayName = "Alice"),
|
||||||
|
aKnockRequest(displayName = "Bob"),
|
||||||
|
aKnockRequest(displayName = "Charlie")
|
||||||
|
),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onViewRequestsClick = it
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.screen_room_multiple_knock_requests_view_all_button_title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on accept on a single request emit the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<KnockRequestsBannerEvents>()
|
||||||
|
rule.setKnockRequestsBannerView(
|
||||||
|
state = aKnockRequestsBannerState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_accept)
|
||||||
|
eventsRecorder.assertSingle(KnockRequestsBannerEvents.AcceptSingleRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on dismiss emit the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<KnockRequestsBannerEvents>()
|
||||||
|
rule.setKnockRequestsBannerView(
|
||||||
|
state = aKnockRequestsBannerState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val close = rule.activity.getString(CommonStrings.action_close)
|
||||||
|
rule.onNodeWithContentDescription(close).performClick()
|
||||||
|
eventsRecorder.assertSingle(KnockRequestsBannerEvents.Dismiss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setKnockRequestsBannerView(
|
||||||
|
state: KnockRequestsBannerState,
|
||||||
|
onViewRequestsClick: () -> Unit = EnsureNeverCalled(),
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
KnockRequestsBannerView(
|
||||||
|
state = state,
|
||||||
|
onViewRequestsClick = onViewRequestsClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,310 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
|
package io.element.android.features.knockrequests.impl.list
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.knockrequests.impl.data.KnockRequestsService
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.test.room.knock.FakeKnockRequest
|
||||||
|
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.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class KnockRequestsListPresenterTest {
|
||||||
|
@Test
|
||||||
|
fun `present - initial states should be emitted`() = runTest {
|
||||||
|
val presenter = createKnockRequestsListPresenter()
|
||||||
|
presenter.test {
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.knockRequests).isInstanceOf(AsyncData.Loading::class.java)
|
||||||
|
assertThat(state.canAccept).isFalse()
|
||||||
|
assertThat(state.canDecline).isFalse()
|
||||||
|
assertThat(state.canBan).isFalse()
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.knockRequests).isInstanceOf(AsyncData.Loading::class.java)
|
||||||
|
assertThat(state.canAccept).isTrue()
|
||||||
|
assertThat(state.canDecline).isTrue()
|
||||||
|
assertThat(state.canBan).isTrue()
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.knockRequests).isInstanceOf(AsyncData.Success::class.java)
|
||||||
|
assertThat(state.knockRequests.dataOrNull()).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - accept success scenario`() = runTest {
|
||||||
|
val acceptLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||||
|
val knockRequest = FakeKnockRequest(acceptLambda = acceptLambda)
|
||||||
|
val knockRequests = flowOf(listOf(knockRequest))
|
||||||
|
val presenter = createKnockRequestsListPresenter(
|
||||||
|
knockRequestsFlow = knockRequests
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
|
||||||
|
state.eventSink(KnockRequestsListEvents.Accept(knockRequestPresentable))
|
||||||
|
}
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
|
||||||
|
assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.Accept(knockRequestPresentable))
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||||
|
state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
|
||||||
|
}
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
|
assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.None)
|
||||||
|
assertThat(state.knockRequests.dataOrNull().orEmpty()).isEmpty()
|
||||||
|
}
|
||||||
|
assert(acceptLambda).isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - accept failure scenario`() = runTest {
|
||||||
|
val acceptLambda = lambdaRecorder<Result<Unit>> { Result.failure(Exception()) }
|
||||||
|
val knockRequest = FakeKnockRequest(acceptLambda = acceptLambda)
|
||||||
|
val knockRequests = flowOf(listOf(knockRequest))
|
||||||
|
val presenter = createKnockRequestsListPresenter(
|
||||||
|
knockRequestsFlow = knockRequests
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
|
||||||
|
state.eventSink(KnockRequestsListEvents.Accept(knockRequestPresentable))
|
||||||
|
}
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
|
||||||
|
assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.Accept(knockRequestPresentable))
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
|
state.eventSink(KnockRequestsListEvents.RetryCurrentAction)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
|
state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
|
||||||
|
}
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
|
assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.None)
|
||||||
|
assertThat(state.knockRequests.dataOrNull()).hasSize(1)
|
||||||
|
}
|
||||||
|
assert(acceptLambda).isCalledExactly(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - decline success scenario`() = runTest {
|
||||||
|
val declineLambda = lambdaRecorder<String?, Result<Unit>> { Result.success(Unit) }
|
||||||
|
val knockRequest = FakeKnockRequest(declineLambda = declineLambda)
|
||||||
|
val knockRequests = flowOf(listOf(knockRequest))
|
||||||
|
val presenter = createKnockRequestsListPresenter(
|
||||||
|
knockRequestsFlow = knockRequests
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
|
||||||
|
state.eventSink(KnockRequestsListEvents.Decline(knockRequestPresentable))
|
||||||
|
}
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
|
||||||
|
assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.Decline(knockRequestPresentable))
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java)
|
||||||
|
state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||||
|
state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
|
||||||
|
}
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
|
assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.None)
|
||||||
|
assertThat(state.knockRequests.dataOrNull().orEmpty()).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert(declineLambda).isCalledOnce()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - decline and ban success scenario`() = runTest {
|
||||||
|
val declineAndBanLambda = lambdaRecorder<String?, Result<Unit>> { Result.success(Unit) }
|
||||||
|
val knockRequest = FakeKnockRequest(declineAndBanLambda = declineAndBanLambda)
|
||||||
|
val knockRequests = flowOf(listOf(knockRequest))
|
||||||
|
val presenter = createKnockRequestsListPresenter(
|
||||||
|
knockRequestsFlow = knockRequests
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
|
||||||
|
state.eventSink(KnockRequestsListEvents.DeclineAndBan(knockRequestPresentable))
|
||||||
|
}
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
|
||||||
|
assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.DeclineAndBan(knockRequestPresentable))
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java)
|
||||||
|
state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||||
|
state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
|
||||||
|
}
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
|
assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.None)
|
||||||
|
assertThat(state.knockRequests.dataOrNull().orEmpty()).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert(declineAndBanLambda).isCalledOnce()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - accept all success scenario`() = runTest {
|
||||||
|
val acceptLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||||
|
val knockRequests = flowOf(
|
||||||
|
listOf(
|
||||||
|
FakeKnockRequest(eventId = AN_EVENT_ID, acceptLambda = acceptLambda),
|
||||||
|
FakeKnockRequest(eventId = AN_EVENT_ID_2, acceptLambda = acceptLambda),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val presenter = createKnockRequestsListPresenter(
|
||||||
|
knockRequestsFlow = knockRequests
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.canAcceptAll).isTrue()
|
||||||
|
state.eventSink(KnockRequestsListEvents.AcceptAll)
|
||||||
|
}
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.AcceptAll)
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java)
|
||||||
|
state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||||
|
state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
|
||||||
|
}
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
|
assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.None)
|
||||||
|
assertThat(state.knockRequests.dataOrNull().orEmpty()).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert(acceptLambda).isCalledExactly(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - accept all partial success scenario`() = runTest {
|
||||||
|
val acceptSuccessLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||||
|
val acceptFailureLambda = lambdaRecorder<Result<Unit>> { Result.failure(Exception()) }
|
||||||
|
val knockRequests = flowOf(
|
||||||
|
listOf(
|
||||||
|
FakeKnockRequest(eventId = AN_EVENT_ID, acceptLambda = acceptSuccessLambda),
|
||||||
|
FakeKnockRequest(eventId = AN_EVENT_ID_2, acceptLambda = acceptFailureLambda),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val presenter = createKnockRequestsListPresenter(
|
||||||
|
knockRequestsFlow = knockRequests
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.canAcceptAll).isTrue()
|
||||||
|
state.eventSink(KnockRequestsListEvents.AcceptAll)
|
||||||
|
}
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.AcceptAll)
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java)
|
||||||
|
state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
|
state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
|
||||||
|
}
|
||||||
|
skipItems(2)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
|
assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.None)
|
||||||
|
assertThat(state.knockRequests.dataOrNull()).hasSize(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert(acceptFailureLambda).isCalledOnce()
|
||||||
|
assert(acceptSuccessLambda).isCalledOnce()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createKnockRequestsListPresenter(
|
||||||
|
canAccept: Boolean = true,
|
||||||
|
canDecline: Boolean = true,
|
||||||
|
canBan: Boolean = true,
|
||||||
|
knockRequestsFlow: Flow<List<KnockRequest>> = flowOf(emptyList())
|
||||||
|
): KnockRequestsListPresenter {
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
canInviteResult = { Result.success(canAccept) },
|
||||||
|
canKickResult = { Result.success(canDecline) },
|
||||||
|
canBanResult = { Result.success(canBan) }
|
||||||
|
)
|
||||||
|
val knockRequestsService = KnockRequestsService(
|
||||||
|
knockRequestsFlow = knockRequestsFlow,
|
||||||
|
coroutineScope = backgroundScope
|
||||||
|
)
|
||||||
|
return KnockRequestsListPresenter(
|
||||||
|
room = room,
|
||||||
|
knockRequestsService = knockRequestsService,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.knockrequests.impl.list
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.features.knockrequests.impl.R
|
||||||
|
import io.element.android.features.knockrequests.impl.data.aKnockRequest
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
|
import io.element.android.tests.testutils.pressBack
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class KnockRequestsListViewTest {
|
||||||
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on back invoke the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setKnockRequestsListView(
|
||||||
|
aKnockRequestsListState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onBackClick = it
|
||||||
|
)
|
||||||
|
rule.pressBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on accept emit the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
|
||||||
|
val knockRequest = aKnockRequest()
|
||||||
|
rule.setKnockRequestsListView(
|
||||||
|
aKnockRequestsListState(
|
||||||
|
knockRequests = AsyncData.Success(persistentListOf(knockRequest)),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_accept)
|
||||||
|
eventsRecorder.assertSingle(KnockRequestsListEvents.Accept(knockRequest))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on decline emit the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
|
||||||
|
val knockRequest = aKnockRequest()
|
||||||
|
rule.setKnockRequestsListView(
|
||||||
|
aKnockRequestsListState(
|
||||||
|
knockRequests = AsyncData.Success(persistentListOf(knockRequest)),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_decline)
|
||||||
|
eventsRecorder.assertSingle(KnockRequestsListEvents.Decline(knockRequest))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on decline and ban emit the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
|
||||||
|
val knockRequest = aKnockRequest()
|
||||||
|
rule.setKnockRequestsListView(
|
||||||
|
aKnockRequestsListState(
|
||||||
|
knockRequests = AsyncData.Success(persistentListOf(knockRequest)),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.screen_knock_requests_list_decline_and_ban_action_title)
|
||||||
|
eventsRecorder.assertSingle(KnockRequestsListEvents.DeclineAndBan(knockRequest))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on accept all emit the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
|
||||||
|
val knockRequests = persistentListOf(aKnockRequest(), aKnockRequest())
|
||||||
|
rule.setKnockRequestsListView(
|
||||||
|
aKnockRequestsListState(
|
||||||
|
knockRequests = AsyncData.Success(knockRequests),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.screen_knock_requests_list_accept_all_button_title)
|
||||||
|
eventsRecorder.assertSingle(KnockRequestsListEvents.AcceptAll)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `retry on async view retry emit the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
|
||||||
|
val knockRequests = persistentListOf(aKnockRequest(), aKnockRequest())
|
||||||
|
rule.setKnockRequestsListView(
|
||||||
|
aKnockRequestsListState(
|
||||||
|
knockRequests = AsyncData.Success(knockRequests),
|
||||||
|
asyncAction = AsyncAction.Failure(Throwable("Failed to accept all")),
|
||||||
|
actionTarget = KnockRequestsActionTarget.AcceptAll,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_retry)
|
||||||
|
eventsRecorder.assertSingle(KnockRequestsListEvents.RetryCurrentAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `canceling async view emit the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
|
||||||
|
val knockRequests = persistentListOf(aKnockRequest(), aKnockRequest())
|
||||||
|
rule.setKnockRequestsListView(
|
||||||
|
aKnockRequestsListState(
|
||||||
|
knockRequests = AsyncData.Success(knockRequests),
|
||||||
|
asyncAction = AsyncAction.Failure(Throwable("Failed to accept all")),
|
||||||
|
actionTarget = KnockRequestsActionTarget.AcceptAll,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_cancel)
|
||||||
|
eventsRecorder.assertSingle(KnockRequestsListEvents.ResetCurrentAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `confirming async view emit the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
|
||||||
|
val knockRequests = persistentListOf(aKnockRequest(), aKnockRequest())
|
||||||
|
rule.setKnockRequestsListView(
|
||||||
|
aKnockRequestsListState(
|
||||||
|
knockRequests = AsyncData.Success(knockRequests),
|
||||||
|
asyncAction = AsyncAction.ConfirmingNoParams,
|
||||||
|
actionTarget = KnockRequestsActionTarget.AcceptAll,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.screen_knock_requests_list_accept_all_alert_confirm_button_title)
|
||||||
|
eventsRecorder.assertSingle(KnockRequestsListEvents.ConfirmCurrentAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setKnockRequestsListView(
|
||||||
|
state: KnockRequestsListState,
|
||||||
|
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
KnockRequestsListView(
|
||||||
|
state = state,
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,37 +7,42 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.test.room.knock
|
package io.element.android.libraries.matrix.test.room.knock
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
|
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
|
||||||
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.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
|
||||||
class FakeKnockRequest(
|
class FakeKnockRequest(
|
||||||
|
override val eventId: EventId = AN_EVENT_ID,
|
||||||
override val userId: UserId = A_USER_ID,
|
override val userId: UserId = A_USER_ID,
|
||||||
override val displayName: String? = A_USER_NAME,
|
override val displayName: String? = A_USER_NAME,
|
||||||
override val avatarUrl: String? = AN_AVATAR_URL,
|
override val avatarUrl: String? = AN_AVATAR_URL,
|
||||||
override val reason: String? = null,
|
override val reason: String? = null,
|
||||||
override val timestamp: Long? = null,
|
override val timestamp: Long? = null,
|
||||||
|
override val isSeen: Boolean = false,
|
||||||
val acceptLambda: () -> Result<Unit> = { lambdaError() },
|
val acceptLambda: () -> Result<Unit> = { lambdaError() },
|
||||||
val declineLambda: (String?) -> Result<Unit> = { lambdaError() },
|
val declineLambda: (String?) -> Result<Unit> = { lambdaError() },
|
||||||
val declineAndBanLambda: (String?) -> Result<Unit> = { lambdaError() },
|
val declineAndBanLambda: (String?) -> Result<Unit> = { lambdaError() },
|
||||||
val markAsSeenLambda: () -> Result<Unit> = { lambdaError() },
|
val markAsSeenLambda: () -> Result<Unit> = { lambdaError() },
|
||||||
) : KnockRequest {
|
) : KnockRequest {
|
||||||
override suspend fun accept(): Result<Unit> {
|
override suspend fun accept(): Result<Unit> = simulateLongTask{
|
||||||
return acceptLambda()
|
acceptLambda()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun decline(reason: String?): Result<Unit> {
|
override suspend fun decline(reason: String?): Result<Unit> = simulateLongTask {
|
||||||
return declineLambda(reason)
|
declineLambda(reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun declineAndBan(reason: String?): Result<Unit> {
|
override suspend fun declineAndBan(reason: String?): Result<Unit> = simulateLongTask {
|
||||||
return declineAndBanLambda(reason)
|
declineAndBanLambda(reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun markAsSeen(): Result<Unit> {
|
override suspend fun markAsSeen(): Result<Unit> = simulateLongTask {
|
||||||
return markAsSeenLambda()
|
markAsSeenLambda()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue