Merge pull request #2413 from element-hq/feature/bma/unitTests

Add more unit tests
This commit is contained in:
Benoit Marty 2024-02-19 16:15:28 +01:00 committed by GitHub
commit 354e82d489
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 463 additions and 92 deletions

View file

@ -22,6 +22,11 @@ plugins {
android {
namespace = "io.element.android.features.location.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
anvil {
@ -51,11 +56,14 @@ dependencies {
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.robolectric)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(libs.test.truth)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.testtags)
testImplementation(projects.services.analytics.test)
testImplementation(projects.features.messages.test)
testImplementation(projects.tests.testutils)
testImplementation(libs.androidx.compose.ui.test.junit)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
}

View file

@ -24,78 +24,47 @@ private const val APP_NAME = "ApplicationName"
class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
override val values: Sequence<ShowLocationState>
get() = sequenceOf(
ShowLocationState(
ShowLocationState.Dialog.None,
Location(1.23, 2.34, 4f),
description = null,
hasLocationPermission = false,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
aShowLocationState(),
aShowLocationState(
permissionDialog = ShowLocationState.Dialog.PermissionDenied,
),
ShowLocationState(
ShowLocationState.Dialog.PermissionDenied,
Location(1.23, 2.34, 4f),
description = null,
hasLocationPermission = false,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
aShowLocationState(
permissionDialog = ShowLocationState.Dialog.PermissionRationale,
),
ShowLocationState(
ShowLocationState.Dialog.PermissionRationale,
Location(1.23, 2.34, 4f),
description = null,
hasLocationPermission = false,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
),
ShowLocationState(
ShowLocationState.Dialog.None,
Location(1.23, 2.34, 4f),
description = null,
aShowLocationState(
hasLocationPermission = true,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
),
ShowLocationState(
ShowLocationState.Dialog.None,
Location(1.23, 2.34, 4f),
description = null,
aShowLocationState(
hasLocationPermission = true,
isTrackMyLocation = true,
appName = APP_NAME,
eventSink = {},
),
ShowLocationState(
ShowLocationState.Dialog.None,
Location(1.23, 2.34, 4f),
aShowLocationState(
description = "My favourite place!",
hasLocationPermission = false,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
),
ShowLocationState(
ShowLocationState.Dialog.None,
Location(1.23, 2.34, 4f),
aShowLocationState(
description = "For some reason I decided to to write a small essay that wraps at just two lines!",
hasLocationPermission = false,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
),
ShowLocationState(
ShowLocationState.Dialog.None,
Location(1.23, 2.34, 4f),
aShowLocationState(
description = "For some reason I decided to write a small essay in the location description. " +
"It is so long that it will wrap onto more than two lines!",
hasLocationPermission = false,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
),
)
}
fun aShowLocationState(
permissionDialog: ShowLocationState.Dialog = ShowLocationState.Dialog.None,
location: Location = Location(1.23, 2.34, 4f),
description: String? = null,
hasLocationPermission: Boolean = false,
isTrackMyLocation: Boolean = false,
appName: String = APP_NAME,
eventSink: (ShowLocationEvents) -> Unit = {},
) = ShowLocationState(
permissionDialog = permissionDialog,
location = location,
description = description,
hasLocationPermission = hasLocationPermission,
isTrackMyLocation = isTrackMyLocation,
appName = appName,
eventSink = eventSink,
)

View file

@ -120,10 +120,14 @@ fun ShowLocationView(
)
},
navigationIcon = {
BackButton(onClick = onBackPressed)
BackButton(
onClick = onBackPressed,
)
},
actions = {
IconButton(onClick = { state.eventSink(ShowLocationEvents.Share) }) {
IconButton(
onClick = { state.eventSink(ShowLocationEvents.Share) }
) {
Icon(
imageVector = CompoundIcons.ShareAndroid(),
contentDescription = stringResource(CommonStrings.action_share),

View file

@ -0,0 +1,156 @@
/*
* 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.location.impl.show
import androidx.activity.ComponentActivity
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalInspectionMode
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.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
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.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ShowLocationViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `test back action`() {
val eventsRecorder = EventsRecorder<ShowLocationEvents>(expectEvents = false)
ensureCalledOnce { callback ->
rule.setShowLocationView(
state = aShowLocationState(
eventSink = eventsRecorder
),
onBackPressed = callback,
)
rule.pressBack()
}
}
@Test
fun `test share action`() {
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
rule.setShowLocationView(
aShowLocationState(
eventSink = eventsRecorder
),
onBackPressed = EnsureNeverCalled(),
)
val shareContentDescription = rule.activity.getString(CommonStrings.action_share)
rule.onNodeWithContentDescription(shareContentDescription).performClick()
eventsRecorder.assertSingle(ShowLocationEvents.Share)
}
@Test
fun `test fab click`() {
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
rule.setShowLocationView(
aShowLocationState(
eventSink = eventsRecorder
),
onBackPressed = EnsureNeverCalled(),
)
val shareContentDescription = rule.activity.getString(CommonStrings.action_share)
rule.onNodeWithTag(TestTags.floatingActionButton.value).performClick()
eventsRecorder.assertSingle(ShowLocationEvents.TrackMyLocation(true))
}
@Test
fun `when permission denied is displayed user can open the settings`() {
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
rule.setShowLocationView(
aShowLocationState(
permissionDialog = ShowLocationState.Dialog.PermissionDenied,
eventSink = eventsRecorder
),
onBackPressed = EnsureNeverCalled(),
)
rule.clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(ShowLocationEvents.OpenAppSettings)
}
@Test
fun `when permission denied is displayed user can close the dialog`() {
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
rule.setShowLocationView(
aShowLocationState(
permissionDialog = ShowLocationState.Dialog.PermissionDenied,
eventSink = eventsRecorder
),
onBackPressed = EnsureNeverCalled(),
)
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(ShowLocationEvents.DismissDialog)
}
@Test
fun `when permission rationale is displayed user can request permissions`() {
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
rule.setShowLocationView(
aShowLocationState(
permissionDialog = ShowLocationState.Dialog.PermissionRationale,
eventSink = eventsRecorder
),
onBackPressed = EnsureNeverCalled(),
)
rule.clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(ShowLocationEvents.RequestPermissions)
}
@Test
fun `when permission rationale is displayed user can close the dialog`() {
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
rule.setShowLocationView(
aShowLocationState(
permissionDialog = ShowLocationState.Dialog.PermissionRationale,
eventSink = eventsRecorder
),
onBackPressed = EnsureNeverCalled(),
)
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(ShowLocationEvents.DismissDialog)
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setShowLocationView(
state: ShowLocationState,
onBackPressed: () -> Unit = EnsureNeverCalled(),
) {
setContent {
// Simulate a LocalInspectionMode for MapboxMap
CompositionLocalProvider(LocalInspectionMode provides true) {
ShowLocationView(
state = state,
onBackPressed = onBackPressed,
)
}
}
}

View file

@ -49,6 +49,7 @@ import io.element.android.features.messages.impl.voicemessages.timeline.FakeReda
import io.element.android.features.messages.test.FakeMessageComposerContext
import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.features.poll.api.actions.EndPollAction
import io.element.android.features.poll.test.actions.FakeEndPollAction
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
@ -98,6 +99,7 @@ import io.element.android.tests.testutils.testCoroutineDispatchers
import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@ -372,6 +374,22 @@ class MessagesPresenterTest {
}
}
@Test
fun `present - handle action end poll`() = runTest {
val endPollAction = FakeEndPollAction()
val presenter = createMessagesPresenter(endPollAction = endPollAction)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
endPollAction.verifyExecutionCount(0)
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent(content = aTimelineItemPollContent())))
delay(1)
endPollAction.verifyExecutionCount(1)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - handle action redact`() = runTest {
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
@ -683,6 +701,7 @@ class MessagesPresenterTest {
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
endPollAction: EndPollAction = FakeEndPollAction(),
): MessagesPresenter {
val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom)
val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter)
@ -721,7 +740,7 @@ class MessagesPresenterTest {
encryptionService = FakeEncryptionService(),
verificationService = FakeSessionVerificationService(),
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
endPollAction = FakeEndPollAction(),
endPollAction = endPollAction,
sendPollResponseAction = FakeSendPollResponseAction(),
sessionPreferencesStore = sessionPreferencesStore,
)

View file

@ -87,10 +87,13 @@ import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import uniffi.wysiwyg_composer.MentionsState
import java.io.File
@Suppress("LargeClass")
@RunWith(RobolectricTestRunner::class)
class MessageComposerPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@ -875,6 +878,19 @@ class MessageComposerPresenterTest {
}
}
@Test
fun `present - send uri`() = runTest {
val presenter = createPresenter(this)
moleculeFlow(RecompositionMode.Immediate) {
val state = presenter.present()
remember(state, state.richTextEditorState.messageHtml) { state }
}.test {
val initialState = awaitFirstItem()
initialState.eventSink.invoke(MessageComposerEvents.SendUri(Uri.parse("content://uri")))
waitForPredicate { mediaPreProcessor.processCallCount == 1 }
}
}
@Test
fun `present - handle typing notice event`() = runTest {
val room = FakeMatrixRoom()

View file

@ -23,6 +23,11 @@ plugins {
android {
namespace = "io.element.android.features.roomdetails.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
anvil {
@ -61,6 +66,7 @@ dependencies {
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(libs.test.mockk)
testImplementation(libs.test.robolectric)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.mediaupload.test)
testImplementation(projects.libraries.mediapickers.test)
@ -70,6 +76,8 @@ dependencies {
testImplementation(projects.tests.testutils)
testImplementation(projects.features.leaveroom.test)
testImplementation(projects.features.createroom.test)
testImplementation(libs.androidx.compose.ui.test.junit)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
ksp(libs.showkase.processor)
}

View file

@ -55,7 +55,10 @@ fun BlockUserDialogs(state: RoomMemberDetailsState) {
}
@Composable
private fun BlockConfirmationDialog(onBlockAction: () -> Unit, onDismiss: () -> Unit) {
private fun BlockConfirmationDialog(
onBlockAction: () -> Unit,
onDismiss: () -> Unit,
) {
ConfirmationDialog(
title = stringResource(R.string.screen_dm_details_block_user),
content = stringResource(R.string.screen_dm_details_block_alert_description),
@ -66,7 +69,10 @@ private fun BlockConfirmationDialog(onBlockAction: () -> Unit, onDismiss: () ->
}
@Composable
private fun UnblockConfirmationDialog(onUnblockAction: () -> Unit, onDismiss: () -> Unit) {
private fun UnblockConfirmationDialog(
onUnblockAction: () -> Unit,
onDismiss: () -> Unit,
) {
ConfirmationDialog(
title = stringResource(R.string.screen_dm_details_unblock_user),
content = stringResource(R.string.screen_dm_details_unblock_alert_description),

View file

@ -19,28 +19,38 @@ package io.element.android.features.roomdetails.impl.members.details
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.RoomId
open class RoomMemberDetailsStateProvider : PreviewParameterProvider<RoomMemberDetailsState> {
override val values: Sequence<RoomMemberDetailsState>
get() = sequenceOf(
aRoomMemberDetailsState(),
aRoomMemberDetailsState().copy(userName = null),
aRoomMemberDetailsState().copy(isBlocked = AsyncData.Success(true)),
aRoomMemberDetailsState().copy(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block),
aRoomMemberDetailsState().copy(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock),
aRoomMemberDetailsState().copy(isBlocked = AsyncData.Loading(true)),
aRoomMemberDetailsState().copy(startDmActionState = AsyncAction.Loading),
aRoomMemberDetailsState(userName = null),
aRoomMemberDetailsState(isBlocked = AsyncData.Success(true)),
aRoomMemberDetailsState(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block),
aRoomMemberDetailsState(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock),
aRoomMemberDetailsState(isBlocked = AsyncData.Loading(true)),
aRoomMemberDetailsState(startDmActionState = AsyncAction.Loading),
// Add other states here
)
}
fun aRoomMemberDetailsState() = RoomMemberDetailsState(
userId = "@daniel:domain.com",
userName = "Daniel",
avatarUrl = null,
isBlocked = AsyncData.Success(false),
startDmActionState = AsyncAction.Uninitialized,
displayConfirmationDialog = null,
isCurrentUser = false,
eventSink = {},
fun aRoomMemberDetailsState(
userId: String = "@daniel:domain.com",
userName: String? = "Daniel",
avatarUrl: String? = null,
isBlocked: AsyncData<Boolean> = AsyncData.Success(false),
startDmActionState: AsyncAction<RoomId> = AsyncAction.Uninitialized,
displayConfirmationDialog: RoomMemberDetailsState.ConfirmationDialog? = null,
isCurrentUser: Boolean = false,
eventSink: (RoomMemberDetailsEvents) -> Unit = {},
) = RoomMemberDetailsState(
userId = userId,
userName = userName,
avatarUrl = avatarUrl,
isBlocked = isBlocked,
startDmActionState = startDmActionState,
displayConfirmationDialog = displayConfirmationDialog,
isCurrentUser = isCurrentUser,
eventSink = eventSink,
)

View file

@ -0,0 +1,96 @@
/*
* 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.roomdetails.impl.blockuser
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.roomdetails.impl.R
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
import io.element.android.features.roomdetails.impl.members.details.aRoomMemberDetailsState
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class BlockUserDialogsTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `confirm block user emit expected Event`() {
val eventsRecorder = EventsRecorder<RoomMemberDetailsEvents>()
rule.setContent {
BlockUserDialogs(
state = aRoomMemberDetailsState(
displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block,
eventSink = eventsRecorder,
)
)
}
rule.clickOn(R.string.screen_dm_details_block_alert_action)
eventsRecorder.assertSingle(RoomMemberDetailsEvents.BlockUser(false))
}
@Test
fun `cancel block user emit expected Event`() {
val eventsRecorder = EventsRecorder<RoomMemberDetailsEvents>()
rule.setContent {
BlockUserDialogs(
state = aRoomMemberDetailsState(
displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block,
eventSink = eventsRecorder,
)
)
}
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(RoomMemberDetailsEvents.ClearConfirmationDialog)
}
@Test
fun `confirm unblock user emit expected Event`() {
val eventsRecorder = EventsRecorder<RoomMemberDetailsEvents>()
rule.setContent {
BlockUserDialogs(
state = aRoomMemberDetailsState(
displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock,
eventSink = eventsRecorder,
)
)
}
rule.clickOn(R.string.screen_dm_details_unblock_alert_action)
eventsRecorder.assertSingle(RoomMemberDetailsEvents.UnblockUser(false))
}
@Test
fun `cancel unblock user emit expected Event`() {
val eventsRecorder = EventsRecorder<RoomMemberDetailsEvents>()
rule.setContent {
BlockUserDialogs(
state = aRoomMemberDetailsState(
displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock,
eventSink = eventsRecorder,
)
)
}
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(RoomMemberDetailsEvents.ClearConfirmationDialog)
}
}

View file

@ -75,13 +75,14 @@ class RoomListPresenter @Inject constructor(
private val inviteStateDataSource: InviteStateDataSource,
private val leaveRoomPresenter: LeaveRoomPresenter,
private val roomListDataSource: RoomListDataSource,
private val encryptionService: EncryptionService,
private val featureFlagService: FeatureFlagService,
private val indicatorService: IndicatorService,
private val searchPresenter: Presenter<RoomListSearchState>,
private val migrationScreenPresenter: MigrationScreenPresenter,
private val sessionPreferencesStore: SessionPreferencesStore,
) : Presenter<RoomListState> {
private val encryptionService: EncryptionService = client.encryptionService()
@Composable
override fun present(): RoomListState {
val coroutineScope = rememberCoroutineScope()
@ -139,7 +140,6 @@ class RoomListPresenter @Inject constructor(
contextMenu.value = RoomListState.ContextMenu.Hidden
}
is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId))
is RoomListEvents.SetRoomIsFavorite -> coroutineScope.launch {
client.getRoom(event.roomId)?.use { room ->
room.setIsFavorite(event.isFavorite)

View file

@ -38,9 +38,10 @@ fun aRoomListSearchState(
isSearchActive: Boolean = false,
query: String = "",
results: ImmutableList<RoomListRoomSummary> = persistentListOf(),
eventSink: (RoomListSearchEvents) -> Unit = { },
) = RoomListSearchState(
isSearchActive = isSearchActive,
query = query,
results = results,
eventSink = { },
eventSink = eventSink,
)

View file

@ -33,6 +33,7 @@ import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryF
import io.element.android.features.roomlist.impl.migration.InMemoryMigrationScreenStore
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
import io.element.android.features.roomlist.impl.model.createRoomListRoomSummary
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
import io.element.android.features.roomlist.impl.search.RoomListSearchState
import io.element.android.features.roomlist.impl.search.aRoomListSearchState
import io.element.android.libraries.architecture.Presenter
@ -42,13 +43,14 @@ import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampF
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
import io.element.android.libraries.indicator.impl.DefaultIndicatorService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.timeline.ReceiptType
@ -68,6 +70,7 @@ import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.testCoroutineDispatchers
@ -108,9 +111,12 @@ class RoomListPresenterTests {
fun `present - show avatar indicator`() = runTest {
val scope = CoroutineScope(coroutineContext + SupervisorJob())
val encryptionService = FakeEncryptionService()
val matrixClient = FakeMatrixClient(
encryptionService = encryptionService,
)
val sessionVerificationService = FakeSessionVerificationService()
val presenter = createRoomListPresenter(
encryptionService = encryptionService,
client = matrixClient,
sessionVerificationService = sessionVerificationService,
coroutineScope = scope
)
@ -255,6 +261,33 @@ class RoomListPresenterTests {
}
}
@Test
fun `present - handle DismissRecoveryKeyPrompt`() = runTest {
val encryptionService = FakeEncryptionService()
val matrixClient = FakeMatrixClient(
encryptionService = encryptionService,
)
val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
val presenter = createRoomListPresenter(
client = matrixClient,
coroutineScope = scope,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.displayRecoveryKeyPrompt).isFalse()
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
val nextState = awaitItem()
assertThat(nextState.displayRecoveryKeyPrompt).isTrue()
nextState.eventSink(RoomListEvents.DismissRecoveryKeyPrompt)
val finalState = awaitItem()
assertThat(finalState.displayRecoveryKeyPrompt).isFalse()
scope.cancel()
}
}
@Test
fun `present - sets invite state`() = runTest {
val inviteStateFlow = MutableStateFlow(InvitesState.NoInvites)
@ -383,6 +416,40 @@ class RoomListPresenterTests {
}
}
@Test
fun `present - toggle search menu`() = runTest {
val eventRecorder = EventsRecorder<RoomListSearchEvents>()
val searchPresenter: Presenter<RoomListSearchState> = Presenter {
aRoomListSearchState(
eventSink = eventRecorder
)
}
val scope = CoroutineScope(coroutineContext + SupervisorJob())
val presenter = createRoomListPresenter(
coroutineScope = scope,
searchPresenter = searchPresenter,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
eventRecorder.assertEmpty()
initialState.eventSink(RoomListEvents.ToggleSearchResults)
eventRecorder.assertSingle(
RoomListSearchEvents.ToggleSearchVisibility
)
initialState.eventSink(RoomListEvents.ToggleSearchResults)
eventRecorder.assertList(
listOf(
RoomListSearchEvents.ToggleSearchVisibility,
RoomListSearchEvents.ToggleSearchVisibility
)
)
scope.cancel()
}
}
@Test
fun `present - change in notification settings updates the summary for decorations`() = runTest {
val userDefinedMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
@ -506,8 +573,8 @@ class RoomListPresenterTests {
givenFormat(A_FORMATTED_DATE)
},
roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(),
encryptionService: EncryptionService = FakeEncryptionService(),
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)),
coroutineScope: CoroutineScope,
migrationScreenPresenter: MigrationScreenPresenter = MigrationScreenPresenter(
matrixClient = client,
@ -531,12 +598,11 @@ class RoomListPresenterTests {
notificationSettingsService = client.notificationSettingsService(),
appScope = coroutineScope
),
encryptionService = encryptionService,
featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)),
featureFlagService = featureFlagService,
indicatorService = DefaultIndicatorService(
sessionVerificationService = sessionVerificationService,
encryptionService = encryptionService,
featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)),
encryptionService = client.encryptionService(),
featureFlagService = featureFlagService,
),
migrationScreenPresenter = migrationScreenPresenter,
searchPresenter = searchPresenter,

View file

@ -33,6 +33,8 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
@Composable
fun FloatingActionButton(
@ -48,7 +50,7 @@ fun FloatingActionButton(
) {
androidx.compose.material3.FloatingActionButton(
onClick = onClick,
modifier = modifier,
modifier = modifier.testTag(TestTags.floatingActionButton),
shape = shape,
containerColor = containerColor,
contentColor = contentColor,

View file

@ -103,6 +103,10 @@ class FakeEncryptionService : EncryptionService {
backupStateStateFlow.emit(state)
}
suspend fun emitRecoveryState(state: RecoveryState) {
recoveryStateStateFlow.emit(state)
}
suspend fun emitEnableRecoveryProgress(state: EnableRecoveryProgress) {
enableRecoveryProgressStateFlow.emit(state)
}

View file

@ -75,6 +75,11 @@ object TestTags {
val dialogNegative = TestTag("dialog-negative")
val dialogNeutral = TestTag("dialog-neutral")
/**
* Floating Action Button.
*/
val floatingActionButton = TestTag("floating-action-button")
/**
* Timeline item.
*/

View file

@ -85,6 +85,8 @@ fun Project.setupKover() {
"*Presenter\$present\$*",
// Forked from compose
"io.element.android.libraries.designsystem.theme.components.bottomsheet.*",
// Test presenter
"io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter",
)
annotatedBy(
"androidx.compose.ui.tooling.preview.Preview",

View file

@ -101,7 +101,6 @@ class RoomListScreen(
notificationSettingsService = matrixClient.notificationSettingsService(),
appScope = Singleton.appScope
),
encryptionService = encryptionService,
indicatorService = DefaultIndicatorService(
sessionVerificationService = sessionVerificationService,
encryptionService = encryptionService,