Let Claude update the tests.

This commit is contained in:
Benoit Marty
2026-09-22 10:21:01 +02:00
committed by Benoit Marty
parent 9b2577f1dd
commit 872bf5a70f
16 changed files with 1352 additions and 1045 deletions
@@ -0,0 +1,198 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.preferences.impl.account
import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.enterprise.test.FakeSessionEnterpriseService
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
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.indicator.api.IndicatorService
import io.element.android.libraries.indicator.test.FakeIndicatorService
import io.element.android.libraries.matrix.api.oauth.AccountManagementAction
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_SESSION_ID
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_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class PreferencesAccountPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
val accountManagementUrlResult = lambdaRecorder<AccountManagementAction?, Result<String?>> { action ->
Result.success("$action url")
}
val matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = accountManagementUrlResult,
)
createPreferencesAccountPresenter(
matrixClient = matrixClient,
sessionEnterpriseService = FakeSessionEnterpriseService(
tweakMasUrlResult = { "tweaked $it" },
),
).test {
val initialState = awaitItem()
assertThat(initialState.myUser).isEqualTo(
MatrixUser(
userId = matrixClient.sessionId,
displayName = A_USER_NAME,
avatarUrl = AN_AVATAR_URL
)
)
assertThat(initialState.showSecureBackupBadge).isFalse()
assertThat(initialState.accountManagementUrl).isNull()
assertThat(initialState.showLinkNewDevice).isFalse()
assertThat(initialState.nbOfBlockedUsers).isEqualTo(0)
assertThat(initialState.showBlockedUsersItem).isFalse()
assertThat(initialState.directLogoutState).isEqualTo(aDirectLogoutState())
assertThat(initialState.snackbarMessage).isNull()
val loadedState = consumeItemsUntilPredicate { it.accountManagementUrl != null }.last()
assertThat(loadedState.showSecureBackup).isFalse()
assertThat(loadedState.canDeactivateAccount).isTrue()
assertThat(loadedState.canReportBug).isTrue()
assertThat(loadedState.accountManagementUrl).isEqualTo("tweaked null url")
accountManagementUrlResult.assertions().isCalledOnce()
.with(value(null))
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - cannot report bug`() = runTest {
createPreferencesAccountPresenter(
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success("") },
),
rageshakeFeatureAvailability = { flowOf(false) },
).test {
assertThat(awaitItem().canReportBug).isFalse()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - number of blocked users`() = runTest {
createPreferencesAccountPresenter(
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success("") },
ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID, A_USER_ID_2)),
),
).test {
val state = consumeItemsUntilPredicate { it.nbOfBlockedUsers == 2 }.last()
assertThat(state.showBlockedUsersItem).isTrue()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - secure backup badge`() = runTest {
val indicatorService = FakeIndicatorService()
createPreferencesAccountPresenter(
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success("") },
),
indicatorService = indicatorService,
).test {
assertThat(awaitItem().showSecureBackupBadge).isFalse()
indicatorService.setShowSettingChatBackupIndicator(true)
consumeItemsUntilPredicate { it.showSecureBackupBadge }
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - secure backup is shown when the session does not need to be verified`() = runTest {
val sessionVerificationService = FakeSessionVerificationService()
createPreferencesAccountPresenter(
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success("") },
),
sessionVerificationService = sessionVerificationService,
).test {
// The session needs to be verified, so the secure backup entry is hidden.
consumeItemsUntilPredicate { !it.showSecureBackup }
sessionVerificationService.emitNeedsSessionVerification(false)
consumeItemsUntilPredicate { it.showSecureBackup }
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - can deactivate account is false if the Matrix client say so`() = runTest {
createPreferencesAccountPresenter(
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { false },
accountManagementUrlResult = { Result.success(null) },
),
).test {
assertThat(awaitItem().canDeactivateAccount).isFalse()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - link new device`() = runTest {
createPreferencesAccountPresenter(
matrixClient = FakeMatrixClient(
sessionId = A_SESSION_ID,
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success(null) },
),
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.QrCodeLogin.key to true)
),
).test {
consumeItemsUntilPredicate { it.showLinkNewDevice }
cancelAndIgnoreRemainingEvents()
}
}
private fun createPreferencesAccountPresenter(
matrixClient: FakeMatrixClient = FakeMatrixClient(),
sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { flowOf(true) },
indicatorService: IndicatorService = FakeIndicatorService(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
sessionEnterpriseService: SessionEnterpriseService = FakeSessionEnterpriseService(),
) = PreferencesAccountPresenter(
matrixClient = matrixClient,
sessionVerificationService = sessionVerificationService,
snackbarDispatcher = SnackbarDispatcher(),
indicatorService = indicatorService,
directLogoutPresenter = { aDirectLogoutState() },
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
featureFlagService = featureFlagService,
sessionEnterpriseService = sessionEnterpriseService,
)
}
@@ -0,0 +1,245 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.preferences.impl.account
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.hasClickAction
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.robolectric.RobolectricTest
import org.junit.Test
class PreferencesAccountViewTest : RobolectricTest() {
@Test
fun `clicking on back invokes back callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback ->
setView(
aPreferencesAccountState(),
onBackClick = callback,
)
pressBack()
}
}
@Test
fun `click on the avatar invokes the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnceWithParam("anAvatarUrl") { callback ->
setView(
aPreferencesAccountState(
myUser = aMatrixUser(avatarUrl = "anAvatarUrl"),
),
onAvatarClick = callback,
)
onNodeWithContentDescription(activity!!.getString(CommonStrings.a11y_user_avatar)).performClick()
}
}
@Test
fun `click on Edit profile invokes the expected callback`() = runAndroidComposeUiTest {
val user = aMatrixUser()
ensureCalledOnceWithParam(user) { callback ->
setView(
aPreferencesAccountState(myUser = user),
onEditProfileClick = callback,
)
clickOn(R.string.screen_edit_profile_title)
}
}
@Test
fun `click on Manage account invokes the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnceWithParam("aUrl") { callback ->
setView(
aPreferencesAccountState(accountManagementUrl = "aUrl"),
onManageAccountClick = callback,
)
clickOn(CommonStrings.action_manage_account_and_devices)
}
}
@Test
fun `when accountManagementUrl is null, item is not shown`() = runAndroidComposeUiTest {
setView(aPreferencesAccountState(accountManagementUrl = null))
onNodeWithText(activity!!.getString(CommonStrings.action_manage_account_and_devices)).assertDoesNotExist()
}
@Test
fun `click on Link new devices invokes the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback ->
setView(
aPreferencesAccountState(showLinkNewDevice = true),
onLinkNewDeviceClick = callback,
)
clickOn(CommonStrings.common_link_new_device)
}
}
@Test
fun `when showLinkNewDevice is false, item is not shown`() = runAndroidComposeUiTest {
setView(aPreferencesAccountState(showLinkNewDevice = false))
onNodeWithText(activity!!.getString(CommonStrings.common_link_new_device)).assertDoesNotExist()
}
@Test
fun `click on Blocked users invokes the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback ->
setView(
aPreferencesAccountState(nbOfBlockedUsers = 1),
onOpenBlockedUsers = callback,
)
clickOn(CommonStrings.common_blocked_users)
}
}
@Test
fun `when nbOfBlockedUsers is 0, item is not shown`() = runAndroidComposeUiTest {
setView(aPreferencesAccountState(nbOfBlockedUsers = 0))
onNodeWithText(activity!!.getString(CommonStrings.common_blocked_users)).assertDoesNotExist()
}
@Test
fun `click on Notification invokes the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback ->
setView(
aPreferencesAccountState(),
onOpenNotificationSettings = callback,
)
clickOn(R.string.screen_notification_settings_title)
}
}
@Test
fun `click on Encryption invokes the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback ->
setView(
aPreferencesAccountState(showSecureBackup = true),
onSecureBackupClick = callback,
)
val text = activity!!.getString(CommonStrings.common_encryption)
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
}
}
@Test
fun `when showSecureBackup is false, item is not shown`() = runAndroidComposeUiTest {
setView(aPreferencesAccountState(showSecureBackup = false))
onNodeWithText(activity!!.getString(CommonStrings.common_encryption)).assertDoesNotExist()
}
@Test
fun `click on Moderation and safety invokes the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback ->
setView(
aPreferencesAccountState(),
onModerationAndSafetyClick = callback,
)
val text = activity!!.getString(CommonStrings.common_moderation_and_safety)
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
}
}
@Test
fun `click on Report a problem invokes the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback ->
setView(
aPreferencesAccountState(canReportBug = true),
onOpenRageShake = callback,
)
val text = activity!!.getString(CommonStrings.common_report_a_problem)
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
}
}
@Test
fun `when canReportBug is false, item is not shown`() = runAndroidComposeUiTest {
setView(aPreferencesAccountState(canReportBug = false))
onNodeWithText(activity!!.getString(CommonStrings.common_report_a_problem)).assertDoesNotExist()
}
@Test
fun `click on Remove this device invokes the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback ->
setView(
aPreferencesAccountState(),
onSignOutClick = callback,
)
val text = activity!!.getString(CommonStrings.action_signout)
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
}
}
@Test
fun `click on Deactivate invokes the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback ->
setView(
aPreferencesAccountState(canDeactivateAccount = true),
onDeactivateClick = callback,
)
val text = activity!!.getString(CommonStrings.action_delete_account)
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
}
}
@Test
fun `when canDeactivateAccount is false, item is not shown`() = runAndroidComposeUiTest {
setView(aPreferencesAccountState(canDeactivateAccount = false))
onNodeWithText(activity!!.getString(CommonStrings.action_delete_account)).assertDoesNotExist()
}
}
private fun AndroidComposeUiTest<ComponentActivity>.setView(
state: PreferencesAccountState,
onBackClick: () -> Unit = EnsureNeverCalled(),
onAvatarClick: (String) -> Unit = EnsureNeverCalledWithParam(),
onEditProfileClick: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(),
onSecureBackupClick: () -> Unit = EnsureNeverCalled(),
onManageAccountClick: (url: String) -> Unit = EnsureNeverCalledWithParam(),
onLinkNewDeviceClick: () -> Unit = EnsureNeverCalled(),
onOpenRageShake: () -> Unit = EnsureNeverCalled(),
onModerationAndSafetyClick: () -> Unit = EnsureNeverCalled(),
onOpenNotificationSettings: () -> Unit = EnsureNeverCalled(),
onOpenBlockedUsers: () -> Unit = EnsureNeverCalled(),
onSignOutClick: () -> Unit = EnsureNeverCalled(),
onDeactivateClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
PreferencesAccountView(
state = state,
onBackClick = onBackClick,
onAvatarClick = onAvatarClick,
onEditProfileClick = onEditProfileClick,
onSecureBackupClick = onSecureBackupClick,
onManageAccountClick = onManageAccountClick,
onLinkNewDeviceClick = onLinkNewDeviceClick,
onOpenRageShake = onOpenRageShake,
onModerationAndSafetyClick = onModerationAndSafetyClick,
onOpenNotificationSettings = onOpenNotificationSettings,
onOpenBlockedUsers = onOpenBlockedUsers,
onSignOutClick = onSignOutClick,
onDeactivateClick = onDeactivateClick,
)
}
}
@@ -48,6 +48,18 @@ class AppDeveloperSettingsPageTest : RobolectricTest() {
}
}
@Test
fun `clicking on View source emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AppDeveloperSettingsEvent>()
setAppDeveloperSettingsView(
state = anAppDeveloperSettingsState(
eventSink = eventsRecorder
),
)
clickOn(CommonStrings.action_view_source)
eventsRecorder.assertSingle(AppDeveloperSettingsEvent.SetDeveloperModeEnabled(true))
}
@Config(qualifiers = "h1500dp")
@Test
fun `clicking on element call url open the dialogs and submit emits the expected event`() = runAndroidComposeUiTest {
@@ -47,6 +47,7 @@ class AppDeveloperSettingsPresenterTest {
)
presenter.test {
awaitItem().also { state ->
assertThat(state.isDeveloperModeEnabled).isFalse()
assertThat(state.features).isEmpty()
assertThat(state.customElementCallBaseUrlState).isNotNull()
assertThat(state.customElementCallBaseUrlState.baseUrl).isNull()
@@ -130,6 +131,26 @@ class AppDeveloperSettingsPresenterTest {
}
}
@Test
fun `present - developer mode on off`() = runTest {
val preferences = InMemoryAppPreferencesStore()
val presenter = createAppDeveloperSettingsPresenter(preferencesStore = preferences)
presenter.test {
skipItems(1)
awaitItem().also { state ->
assertThat(state.isDeveloperModeEnabled).isFalse()
state.eventSink(AppDeveloperSettingsEvent.SetDeveloperModeEnabled(true))
}
awaitItem().also { state ->
assertThat(state.isDeveloperModeEnabled).isTrue()
state.eventSink(AppDeveloperSettingsEvent.SetDeveloperModeEnabled(false))
}
awaitItem().also { state ->
assertThat(state.isDeveloperModeEnabled).isFalse()
}
}
}
private fun createAppDeveloperSettingsPresenter(
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(
getAvailableFeaturesResult = { _, _ ->
@@ -0,0 +1,71 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.preferences.impl.location
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class LocationSettingsPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
createLocationSettingsPresenter().test {
with(awaitItem()) {
assertThat(liveLocationMinimumDistanceUpdate).isEqualTo(10)
}
}
}
@Test
fun `present - exposes live location minimum distance from app preferences`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore(
liveLocationMinimumDistanceUpdate = 50,
)
createLocationSettingsPresenter(appPreferencesStore = appPreferencesStore).test {
skipItems(1)
with(awaitItem()) {
assertThat(liveLocationMinimumDistanceUpdate).isEqualTo(50)
}
}
}
@Test
fun `present - saving live location minimum distance updates app preferences`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore(
liveLocationMinimumDistanceUpdate = 10,
)
createLocationSettingsPresenter(appPreferencesStore = appPreferencesStore).test {
with(awaitItem()) {
assertThat(liveLocationMinimumDistanceUpdate).isEqualTo(10)
eventSink(LocationSettingsEvent.SetLiveLocationMinimumDistanceUpdate(42))
}
with(awaitItem()) {
assertThat(liveLocationMinimumDistanceUpdate).isEqualTo(42)
}
}
assertThat(appPreferencesStore.getLiveLocationMinimumDistanceInMetersUpdateFlow().first()).isEqualTo(42)
}
private fun CoroutineScope.createLocationSettingsPresenter(
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
) = LocationSettingsPresenter(
appPreferencesStore = appPreferencesStore,
sessionCoroutineScope = this,
)
}
@@ -0,0 +1,88 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.preferences.impl.location
import androidx.activity.ComponentActivity
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performSemanticsAction
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import io.element.android.features.preferences.impl.R
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.robolectric.RobolectricTest
import org.junit.Test
class LocationSettingsViewTest : RobolectricTest() {
@Test
fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LocationSettingsEvent>(expectEvents = false)
ensureCalledOnce {
setLocationSettingsView(
state = aLocationSettingsState(
eventSink = eventsRecorder
),
onBackClick = it
)
pressBack()
}
}
@Test
fun `the current minimum distance is displayed`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LocationSettingsEvent>(expectEvents = false)
setLocationSettingsView(
state = aLocationSettingsState(
liveLocationMinimumDistanceUpdate = 50,
eventSink = eventsRecorder,
),
)
val text = activity!!.resources.getQuantityString(
R.plurals.screen_advanced_settings_live_location_update_distance,
50,
50,
)
onNodeWithText(text).assertExists()
}
@Test
fun `changing the minimum distance emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LocationSettingsEvent>()
setLocationSettingsView(
state = aLocationSettingsState(
liveLocationMinimumDistanceUpdate = 50,
eventSink = eventsRecorder,
),
)
// `setProgress` invokes `onValueChange` and then `onValueChangeFinish`, which is what the view listens to.
onNode(SemanticsMatcher.keyIsDefined(SemanticsActions.SetProgress))
.performSemanticsAction(SemanticsActions.SetProgress) { it(42f) }
eventsRecorder.assertSingle(LocationSettingsEvent.SetLiveLocationMinimumDistanceUpdate(42))
}
}
private fun AndroidComposeUiTest<ComponentActivity>.setLocationSettingsView(
state: LocationSettingsState,
onBackClick: () -> Unit = EnsureNeverCalled(),
onOpenAppSettingsClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
LocationSettingsView(
state = state,
onBackClick = onBackClick,
onOpenAppSettingsClick = onOpenAppSettingsClick,
)
}
}
@@ -1,393 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.preferences.impl.media
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.compound.theme.Theme
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class AdvancedSettingsPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
val presenter = createAdvancedSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
with(awaitItem()) {
assertThat(isDeveloperModeEnabled).isFalse()
assertThat(isSharePresenceEnabled).isTrue()
assertThat(mediaOptimizationState).isNull()
assertThat(theme).isEqualTo(ThemeOption.System)
assertThat(availableThemeOptions).isEqualTo(
listOf(ThemeOption.System, ThemeOption.Light, ThemeOption.Dark).toImmutableList()
)
assertThat(mediaPreviewConfigState.hideInviteAvatars).isFalse()
assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On)
assertThat(mediaPreviewConfigState.setHideInviteAvatarsAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(mediaPreviewConfigState.setTimelineMediaPreviewAction).isEqualTo(AsyncAction.Uninitialized)
}
// After the initial state, we expect the media optimization state to be set
with(awaitItem()) {
assertThat(mediaOptimizationState).isInstanceOf(MediaOptimizationState.AllMedia::class.java)
assertThat((mediaOptimizationState as MediaOptimizationState.AllMedia).isEnabled).isTrue()
}
}
}
@Test
fun `present - developer mode on off`() = runTest {
val presenter = createAdvancedSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// Skip until the initial data it loaded
skipItems(1)
with(awaitItem()) {
assertThat(isDeveloperModeEnabled).isFalse()
eventSink(MediaSettingsEvent.SetDeveloperModeEnabled(true))
}
with(awaitItem()) {
assertThat(isDeveloperModeEnabled).isTrue()
eventSink(MediaSettingsEvent.SetDeveloperModeEnabled(false))
}
with(awaitItem()) {
assertThat(isDeveloperModeEnabled).isFalse()
}
}
}
@Test
fun `present - share presence off on`() = runTest {
val presenter = createAdvancedSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// Skip until the initial data it loaded
skipItems(1)
with(awaitItem()) {
assertThat(isSharePresenceEnabled).isTrue()
eventSink(MediaSettingsEvent.SetSharePresenceEnabled(false))
}
with(awaitItem()) {
assertThat(isSharePresenceEnabled).isFalse()
eventSink(MediaSettingsEvent.SetSharePresenceEnabled(true))
}
with(awaitItem()) {
assertThat(isSharePresenceEnabled).isTrue()
}
}
}
@Test
fun `present - compress media off on`() = runTest {
val presenter = createAdvancedSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// Skip until the initial data it loaded
skipItems(1)
with(awaitItem()) {
assertThat((mediaOptimizationState as MediaOptimizationState.AllMedia).isEnabled).isTrue()
eventSink(MediaSettingsEvent.SetCompressMedia(false))
}
with(awaitItem()) {
assertThat((mediaOptimizationState as MediaOptimizationState.AllMedia).isEnabled).isFalse()
eventSink(MediaSettingsEvent.SetCompressMedia(true))
}
with(awaitItem()) {
assertThat((mediaOptimizationState as MediaOptimizationState.AllMedia).isEnabled).isTrue()
}
}
}
@Test
fun `present - compress images off on`() = runTest {
val presenter = createAdvancedSettingsPresenter(
featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.SelectableMediaQuality, true)
}
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// Skip until the initial data it loaded
skipItems(1)
with(awaitItem()) {
assertThat((mediaOptimizationState as MediaOptimizationState.Split).compressImages).isTrue()
eventSink(MediaSettingsEvent.SetCompressImages(false))
}
with(awaitItem()) {
assertThat((mediaOptimizationState as MediaOptimizationState.Split).compressImages).isFalse()
eventSink(MediaSettingsEvent.SetCompressImages(true))
}
with(awaitItem()) {
assertThat((mediaOptimizationState as MediaOptimizationState.Split).compressImages).isTrue()
}
}
}
@Test
fun `present - video upload quality selector`() = runTest {
val presenter = createAdvancedSettingsPresenter(
featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.SelectableMediaQuality, true)
}
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// Skip until the initial data it loaded
skipItems(1)
with(awaitItem()) {
assertThat((mediaOptimizationState as MediaOptimizationState.Split).videoPreset).isEqualTo(VideoCompressionPreset.STANDARD)
eventSink(MediaSettingsEvent.SetVideoUploadQuality(VideoCompressionPreset.LOW))
}
with(awaitItem()) {
assertThat((mediaOptimizationState as MediaOptimizationState.Split).videoPreset).isEqualTo(VideoCompressionPreset.LOW)
eventSink(MediaSettingsEvent.SetVideoUploadQuality(VideoCompressionPreset.HIGH))
}
with(awaitItem()) {
assertThat((mediaOptimizationState as MediaOptimizationState.Split).videoPreset).isEqualTo(VideoCompressionPreset.HIGH)
}
}
}
@Test
fun `present - change theme`() = runTest {
val presenter = createAdvancedSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// Skip until the initial data it loaded
skipItems(1)
with(awaitItem()) {
assertThat(theme).isEqualTo(ThemeOption.System)
eventSink(MediaSettingsEvent.SetTheme(ThemeOption.Dark))
}
with(awaitItem()) {
assertThat(theme).isEqualTo(ThemeOption.Dark)
eventSink(MediaSettingsEvent.SetTheme(ThemeOption.Light))
}
with(awaitItem()) {
assertThat(theme).isEqualTo(ThemeOption.Light)
eventSink(MediaSettingsEvent.SetTheme(ThemeOption.System))
}
with(awaitItem()) {
assertThat(theme).isEqualTo(ThemeOption.System)
}
}
}
@Test
fun `present - exposes live location minimum distance from app preferences`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore(
liveLocationMinimumDistanceUpdate = 50,
)
val presenter = createAdvancedSettingsPresenter(appPreferencesStore = appPreferencesStore)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
with(awaitItem()) {
assertThat(liveLocationMinimumDistanceUpdate).isEqualTo(50)
}
}
}
@Test
fun `present - saving live location minimum distance updates app preferences`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore(
liveLocationMinimumDistanceUpdate = 10,
)
val presenter = createAdvancedSettingsPresenter(appPreferencesStore = appPreferencesStore)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
with(awaitItem()) {
assertThat(liveLocationMinimumDistanceUpdate).isEqualTo(10)
eventSink(MediaSettingsEvent.SetLiveLocationMinimumDistanceUpdate(42))
}
with(awaitItem()) {
assertThat(liveLocationMinimumDistanceUpdate).isEqualTo(42)
}
}
}
@Test
fun `present - black theme option shown when feature flag enabled`() = runTest {
val presenter = createAdvancedSettingsPresenter(
featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.AllowBlackTheme, true)
}
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
with(awaitItem()) {
assertThat(availableThemeOptions).contains(ThemeOption.Black)
assertThat(availableThemeOptions).isEqualTo(ThemeOption.entries.toImmutableList())
}
}
}
@Test
fun `present - stored black theme falls back to dark when feature flag disabled`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore().apply {
setTheme(Theme.Black.name)
}
val presenter = createAdvancedSettingsPresenter(appPreferencesStore = appPreferencesStore)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
with(awaitItem()) {
assertThat(theme).isEqualTo(ThemeOption.Dark)
}
}
}
@Test
fun `present - hide invite avatars`() = runTest {
val mediaPreviewStore = FakeMediaPreviewConfigStateStore()
val presenter = createAdvancedSettingsPresenter(mediaPreviewConfigStateStore = mediaPreviewStore)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// Skip until the initial data it loaded
skipItems(1)
with(awaitItem()) {
assertThat(mediaPreviewConfigState.hideInviteAvatars).isFalse()
eventSink(MediaSettingsEvent.SetHideInviteAvatars(true))
}
with(awaitItem()) {
assertThat(mediaPreviewConfigState.hideInviteAvatars).isTrue()
eventSink(MediaSettingsEvent.SetHideInviteAvatars(false))
}
with(awaitItem()) {
assertThat(mediaPreviewConfigState.hideInviteAvatars).isFalse()
}
}
assertThat(mediaPreviewStore.getSetHideInviteAvatarsEvents()).isEqualTo(listOf(true, false))
}
@Test
fun `present - timeline media preview value`() = runTest {
val mediaPreviewStore = FakeMediaPreviewConfigStateStore()
val presenter = createAdvancedSettingsPresenter(mediaPreviewConfigStateStore = mediaPreviewStore)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// Skip until the initial data it loaded
skipItems(1)
with(awaitItem()) {
assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On)
eventSink(MediaSettingsEvent.SetTimelineMediaPreviewValue(MediaPreviewValue.Off))
}
with(awaitItem()) {
assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Off)
eventSink(MediaSettingsEvent.SetTimelineMediaPreviewValue(MediaPreviewValue.Private))
}
with(awaitItem()) {
assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Private)
}
}
assertThat(mediaPreviewStore.getSetTimelineMediaPreviewValueEvents()).isEqualTo(
listOf(MediaPreviewValue.Off, MediaPreviewValue.Private)
)
}
@Test
fun `present - media preview state with custom initial values`() = runTest {
val mediaPreviewStore = FakeMediaPreviewConfigStateStore(
hideInviteAvatarsValue = true,
timelineMediaPreviewValue = MediaPreviewValue.Private
)
val presenter = createAdvancedSettingsPresenter(mediaPreviewConfigStateStore = mediaPreviewStore)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// Skip until the initial data it loaded
skipItems(1)
with(awaitItem()) {
assertThat(mediaPreviewConfigState.hideInviteAvatars).isTrue()
assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Private)
}
}
}
@Test
fun `present - async actions state`() = runTest {
val mediaPreviewStore = FakeMediaPreviewConfigStateStore(
setHideInviteAvatarsActionValue = AsyncAction.Loading,
setTimelineMediaPreviewActionValue = AsyncAction.Success(Unit)
)
val presenter = createAdvancedSettingsPresenter(mediaPreviewConfigStateStore = mediaPreviewStore)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// Skip until the initial data it loaded
skipItems(1)
with(awaitItem()) {
assertThat(mediaPreviewConfigState.setHideInviteAvatarsAction).isEqualTo(AsyncAction.Loading)
assertThat(mediaPreviewConfigState.setTimelineMediaPreviewAction).isEqualTo(AsyncAction.Success(Unit))
}
}
}
private fun CoroutineScope.createAdvancedSettingsPresenter(
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
mediaPreviewConfigStateStore: MediaPreviewConfigStateStore = FakeMediaPreviewConfigStateStore(),
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
) = MediaSettingsPresenter(
appPreferencesStore = appPreferencesStore,
sessionPreferencesStore = sessionPreferencesStore,
mediaPreviewConfigStateStore = mediaPreviewConfigStateStore,
featureFlagService = featureFlagService,
sessionCoroutineScope = this,
)
}
@@ -1,264 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.preferences.impl.media
import androidx.activity.ComponentActivity
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.compose.LocalAnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.assertNoNodeWithText
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.robolectric.RobolectricTest
import kotlinx.collections.immutable.toImmutableList
import org.junit.Test
import org.robolectric.annotation.Config
class AdvancedSettingsViewTest : RobolectricTest() {
@Test
fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MediaSettingsEvent>(expectEvents = false)
ensureCalledOnce {
setAdvancedSettingsView(
state = aMediaSettingsState(
eventSink = eventsRecorder
),
onBackClick = it
)
pressBack()
}
}
@Test
fun `clicking on other theme emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MediaSettingsEvent>()
setAdvancedSettingsView(
state = aMediaSettingsState(
eventSink = eventsRecorder,
),
)
clickOn(CommonStrings.common_appearance)
clickOn(R.string.theme_dark)
eventsRecorder.assertSingle(MediaSettingsEvent.SetTheme(ThemeOption.Dark))
}
@Test
fun `black theme is shown when available`() = runAndroidComposeUiTest {
setAdvancedSettingsView(
state = aMediaSettingsState(
availableThemeOptions = ThemeOption.entries.toImmutableList(),
),
)
clickOn(CommonStrings.common_appearance)
run {
val text = activity!!.getString(R.string.theme_black)
onNodeWithText(text).assertExists()
}
}
@Test
fun `black theme is hidden when unavailable`() = runAndroidComposeUiTest {
setAdvancedSettingsView(
state = aMediaSettingsState(
availableThemeOptions = ThemeOption.entries.filterNot { it == ThemeOption.Black }.toImmutableList(),
),
)
clickOn(CommonStrings.common_appearance)
assertNoNodeWithText(R.string.theme_black)
}
@Test
fun `clicking on View source emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MediaSettingsEvent>()
setAdvancedSettingsView(
state = aMediaSettingsState(
eventSink = eventsRecorder,
),
)
clickOn(CommonStrings.action_view_source)
eventsRecorder.assertSingle(MediaSettingsEvent.SetDeveloperModeEnabled(true))
}
@Test
fun `clicking on Share presence emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MediaSettingsEvent>()
setAdvancedSettingsView(
state = aMediaSettingsState(
eventSink = eventsRecorder,
),
)
clickOn(R.string.screen_advanced_settings_share_presence)
eventsRecorder.assertSingle(MediaSettingsEvent.SetSharePresenceEnabled(true))
}
@Test
fun `clicking on media to enable compression emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MediaSettingsEvent>()
val analyticsService = FakeAnalyticsService()
setAdvancedSettingsView(
state = aMediaSettingsState(
eventSink = eventsRecorder,
),
analyticsService = analyticsService
)
clickOn(R.string.screen_advanced_settings_media_compression_description)
eventsRecorder.assertSingle(MediaSettingsEvent.SetCompressMedia(true))
assertThat(analyticsService.capturedEvents).isEqualTo(
listOf(
Interaction(
name = Interaction.Name.MobileSettingsOptimizeMediaUploadsEnabled
)
)
)
}
@Test
fun `clicking on media to disable compression emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MediaSettingsEvent>()
val analyticsService = FakeAnalyticsService()
setAdvancedSettingsView(
state = aMediaSettingsState(
mediaOptimizationState = MediaOptimizationState.AllMedia(isEnabled = true),
eventSink = eventsRecorder,
),
analyticsService = analyticsService
)
clickOn(R.string.screen_advanced_settings_media_compression_description)
eventsRecorder.assertSingle(MediaSettingsEvent.SetCompressMedia(false))
assertThat(analyticsService.capturedEvents).isEqualTo(
listOf(
Interaction(
name = Interaction.Name.MobileSettingsOptimizeMediaUploadsDisabled
)
)
)
}
@Test
@Config(qualifiers = "h1080dp")
fun `clicking on hide invite avatars emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MediaSettingsEvent>()
setAdvancedSettingsView(
state = aMediaSettingsState(
eventSink = eventsRecorder,
hideInviteAvatars = false
),
)
clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title)
eventsRecorder.assertSingle(MediaSettingsEvent.SetHideInviteAvatars(true))
}
@Test
@Config(qualifiers = "h1080dp")
fun `clicking on timeline media preview always hide emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MediaSettingsEvent>()
setAdvancedSettingsView(
state = aMediaSettingsState(
eventSink = eventsRecorder,
timelineMediaPreviewValue = MediaPreviewValue.On
),
)
clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide)
eventsRecorder.assertSingle(MediaSettingsEvent.SetTimelineMediaPreviewValue(MediaPreviewValue.Off))
}
@Test
@Config(qualifiers = "h1080dp")
fun `clicking on timeline media preview private rooms emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MediaSettingsEvent>()
setAdvancedSettingsView(
state = aMediaSettingsState(
eventSink = eventsRecorder,
timelineMediaPreviewValue = MediaPreviewValue.On
),
)
clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms)
eventsRecorder.assertSingle(MediaSettingsEvent.SetTimelineMediaPreviewValue(MediaPreviewValue.Private))
}
@Test
@Config(qualifiers = "h1080dp")
fun `clicking on timeline media preview always show emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MediaSettingsEvent>()
setAdvancedSettingsView(
state = aMediaSettingsState(
eventSink = eventsRecorder,
timelineMediaPreviewValue = MediaPreviewValue.Off
),
)
clickOn(R.string.screen_advanced_settings_show_media_timeline_always_show)
eventsRecorder.assertSingle(MediaSettingsEvent.SetTimelineMediaPreviewValue(MediaPreviewValue.On))
}
@Test
@Config(qualifiers = "h1080dp")
fun `hide invite avatars toggle is disabled when action is loading`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MediaSettingsEvent>(expectEvents = false)
setAdvancedSettingsView(
state = aMediaSettingsState(
eventSink = eventsRecorder,
hideInviteAvatars = false,
setHideInviteAvatarsAction = AsyncAction.Loading
),
)
// The toggle should be disabled, so clicking should not emit any events
clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title)
}
@Test
@Config(qualifiers = "h1080dp")
fun `timeline media preview options are disabled when action is loading`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MediaSettingsEvent>(expectEvents = false)
setAdvancedSettingsView(
state = aMediaSettingsState(
eventSink = eventsRecorder,
timelineMediaPreviewValue = MediaPreviewValue.On,
setTimelineMediaPreviewAction = AsyncAction.Loading
),
)
// The options should be disabled, so clicking should not emit any events
clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide)
clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms)
}
}
private fun AndroidComposeUiTest<ComponentActivity>.setAdvancedSettingsView(
state: MediaSettingsState,
analyticsService: AnalyticsService = FakeAnalyticsService(),
onBackClick: () -> Unit = EnsureNeverCalled(),
onOpenAppSettings: () -> Unit = EnsureNeverCalled(),
) {
setContent {
CompositionLocalProvider(
LocalAnalyticsService provides analyticsService,
) {
MediaSettingsView(
state = state,
onBackClick = onBackClick,
onOpenAppSettingsClick = onOpenAppSettings
)
}
}
}
@@ -0,0 +1,114 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.preferences.impl.media
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class MediaSettingsPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
createMediaSettingsPresenter().test {
assertThat(awaitItem().mediaOptimizationState).isNull()
// After the initial state, we expect the media optimization state to be set
with(awaitItem()) {
assertThat(mediaOptimizationState).isEqualTo(MediaOptimizationState.AllMedia(isEnabled = true))
}
}
}
@Test
fun `present - compress media off on`() = runTest {
createMediaSettingsPresenter().test {
// Skip until the initial data is loaded
skipItems(1)
with(awaitItem()) {
assertThat((mediaOptimizationState as MediaOptimizationState.AllMedia).isEnabled).isTrue()
eventSink(MediaSettingsEvent.SetCompressMedia(false))
}
with(awaitItem()) {
assertThat((mediaOptimizationState as MediaOptimizationState.AllMedia).isEnabled).isFalse()
eventSink(MediaSettingsEvent.SetCompressMedia(true))
}
with(awaitItem()) {
assertThat((mediaOptimizationState as MediaOptimizationState.AllMedia).isEnabled).isTrue()
}
}
}
@Test
fun `present - split media optimization state when the feature flag is enabled`() = runTest {
createMediaSettingsPresenter(
featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.SelectableMediaQuality, true)
}
).test {
// Skip until the initial data is loaded
skipItems(1)
with(awaitItem()) {
assertThat((mediaOptimizationState as MediaOptimizationState.Split).compressImages).isTrue()
eventSink(MediaSettingsEvent.SetCompressMedia(false))
}
with(awaitItem()) {
assertThat((mediaOptimizationState as MediaOptimizationState.Split).compressImages).isFalse()
eventSink(MediaSettingsEvent.SetCompressMedia(true))
}
with(awaitItem()) {
assertThat((mediaOptimizationState as MediaOptimizationState.Split).compressImages).isTrue()
}
}
}
@Test
fun `present - video upload quality selector`() = runTest {
createMediaSettingsPresenter(
featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.SelectableMediaQuality, true)
}
).test {
// Skip until the initial data is loaded
skipItems(1)
with(awaitItem()) {
assertThat((mediaOptimizationState as MediaOptimizationState.Split).videoPreset).isEqualTo(VideoCompressionPreset.STANDARD)
eventSink(MediaSettingsEvent.SetVideoUploadQuality(VideoCompressionPreset.LOW))
}
with(awaitItem()) {
assertThat((mediaOptimizationState as MediaOptimizationState.Split).videoPreset).isEqualTo(VideoCompressionPreset.LOW)
eventSink(MediaSettingsEvent.SetVideoUploadQuality(VideoCompressionPreset.HIGH))
}
with(awaitItem()) {
assertThat((mediaOptimizationState as MediaOptimizationState.Split).videoPreset).isEqualTo(VideoCompressionPreset.HIGH)
}
}
}
private fun CoroutineScope.createMediaSettingsPresenter(
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
) = MediaSettingsPresenter(
sessionPreferencesStore = sessionPreferencesStore,
featureFlagService = featureFlagService,
sessionCoroutineScope = this,
)
}
@@ -0,0 +1,173 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.preferences.impl.media
import androidx.activity.ComponentActivity
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.compose.LocalAnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.assertNoNodeWithText
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.robolectric.RobolectricTest
import org.junit.Test
class MediaSettingsViewTest : RobolectricTest() {
@Test
fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MediaSettingsEvent>(expectEvents = false)
ensureCalledOnce {
setMediaSettingsView(
state = aMediaSettingsState(
eventSink = eventsRecorder
),
onBackClick = it
)
pressBack()
}
}
@Test
fun `when the media optimization state is not loaded, nothing is rendered`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MediaSettingsEvent>(expectEvents = false)
setMediaSettingsView(
state = aMediaSettingsState(
mediaOptimizationState = null,
eventSink = eventsRecorder,
),
)
assertNoNodeWithText(R.string.screen_advanced_settings_media_compression_title)
assertNoNodeWithText(R.string.screen_advanced_settings_optimise_image_upload_quality_title)
}
@Test
fun `clicking on media to enable compression emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MediaSettingsEvent>()
val analyticsService = FakeAnalyticsService()
setMediaSettingsView(
state = aMediaSettingsState(
eventSink = eventsRecorder,
),
analyticsService = analyticsService
)
clickOn(R.string.screen_advanced_settings_media_compression_description)
eventsRecorder.assertSingle(MediaSettingsEvent.SetCompressMedia(true))
assertThat(analyticsService.capturedEvents).isEqualTo(
listOf(
Interaction(
name = Interaction.Name.MobileSettingsOptimizeMediaUploadsEnabled
)
)
)
}
@Test
fun `clicking on media to disable compression emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MediaSettingsEvent>()
val analyticsService = FakeAnalyticsService()
setMediaSettingsView(
state = aMediaSettingsState(
mediaOptimizationState = MediaOptimizationState.AllMedia(isEnabled = true),
eventSink = eventsRecorder,
),
analyticsService = analyticsService
)
clickOn(R.string.screen_advanced_settings_media_compression_description)
eventsRecorder.assertSingle(MediaSettingsEvent.SetCompressMedia(false))
assertThat(analyticsService.capturedEvents).isEqualTo(
listOf(
Interaction(
name = Interaction.Name.MobileSettingsOptimizeMediaUploadsDisabled
)
)
)
}
@Test
fun `clicking on image upload quality in split mode emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MediaSettingsEvent>()
setMediaSettingsView(
state = aMediaSettingsState(
mediaOptimizationState = MediaOptimizationState.Split(
compressImages = false,
videoPreset = VideoCompressionPreset.STANDARD,
),
eventSink = eventsRecorder,
),
)
clickOn(R.string.screen_advanced_settings_optimise_image_upload_quality_title)
eventsRecorder.assertSingle(MediaSettingsEvent.SetCompressMedia(true))
}
@Test
fun `selecting a video upload quality emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MediaSettingsEvent>()
setMediaSettingsView(
state = aMediaSettingsState(
mediaOptimizationState = MediaOptimizationState.Split(
compressImages = true,
videoPreset = VideoCompressionPreset.STANDARD,
),
eventSink = eventsRecorder,
),
)
clickOn(R.string.screen_advanced_settings_optimise_video_upload_quality_title)
clickOn(R.string.screen_advanced_settings_optimise_video_upload_quality_low, inDialog = true)
clickOn(CommonStrings.action_ok, inDialog = true)
eventsRecorder.assertSingle(MediaSettingsEvent.SetVideoUploadQuality(VideoCompressionPreset.LOW))
}
@Test
fun `cancelling the video upload quality dialog emits no event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MediaSettingsEvent>(expectEvents = false)
setMediaSettingsView(
state = aMediaSettingsState(
mediaOptimizationState = MediaOptimizationState.Split(
compressImages = true,
videoPreset = VideoCompressionPreset.STANDARD,
),
eventSink = eventsRecorder,
),
)
clickOn(R.string.screen_advanced_settings_optimise_video_upload_quality_title)
clickOn(R.string.screen_advanced_settings_optimise_video_upload_quality_low, inDialog = true)
clickOn(CommonStrings.action_cancel, inDialog = true)
}
}
private fun AndroidComposeUiTest<ComponentActivity>.setMediaSettingsView(
state: MediaSettingsState,
analyticsService: AnalyticsService = FakeAnalyticsService(),
onBackClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
CompositionLocalProvider(
LocalAnalyticsService provides analyticsService,
) {
MediaSettingsView(
state = state,
onBackClick = onBackClick,
)
}
}
}
@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.preferences.impl.media
package io.element.android.features.preferences.impl.moderation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.preferences.impl.media
package io.element.android.features.preferences.impl.moderation
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
@@ -0,0 +1,131 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.preferences.impl.moderation
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class ModerationAndSafetyPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
createModerationAndSafetyPresenter().test {
with(awaitItem()) {
assertThat(isSharePresenceEnabled).isTrue()
assertThat(mediaPreviewConfigState.hideInviteAvatars).isFalse()
assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On)
assertThat(mediaPreviewConfigState.setHideInviteAvatarsAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(mediaPreviewConfigState.setTimelineMediaPreviewAction).isEqualTo(AsyncAction.Uninitialized)
}
}
}
@Test
fun `present - share presence off on`() = runTest {
createModerationAndSafetyPresenter().test {
with(awaitItem()) {
assertThat(isSharePresenceEnabled).isTrue()
eventSink(ModerationAndSafetyEvent.SetSharePresenceEnabled(false))
}
with(awaitItem()) {
assertThat(isSharePresenceEnabled).isFalse()
eventSink(ModerationAndSafetyEvent.SetSharePresenceEnabled(true))
}
with(awaitItem()) {
assertThat(isSharePresenceEnabled).isTrue()
}
}
}
@Test
fun `present - hide invite avatars`() = runTest {
val mediaPreviewStore = FakeMediaPreviewConfigStateStore()
createModerationAndSafetyPresenter(mediaPreviewConfigStateStore = mediaPreviewStore).test {
with(awaitItem()) {
assertThat(mediaPreviewConfigState.hideInviteAvatars).isFalse()
eventSink(ModerationAndSafetyEvent.SetHideInviteAvatars(true))
}
with(awaitItem()) {
assertThat(mediaPreviewConfigState.hideInviteAvatars).isTrue()
eventSink(ModerationAndSafetyEvent.SetHideInviteAvatars(false))
}
with(awaitItem()) {
assertThat(mediaPreviewConfigState.hideInviteAvatars).isFalse()
}
}
assertThat(mediaPreviewStore.getSetHideInviteAvatarsEvents()).isEqualTo(listOf(true, false))
}
@Test
fun `present - timeline media preview value`() = runTest {
val mediaPreviewStore = FakeMediaPreviewConfigStateStore()
createModerationAndSafetyPresenter(mediaPreviewConfigStateStore = mediaPreviewStore).test {
with(awaitItem()) {
assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On)
eventSink(ModerationAndSafetyEvent.SetTimelineMediaPreviewValue(MediaPreviewValue.Off))
}
with(awaitItem()) {
assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Off)
eventSink(ModerationAndSafetyEvent.SetTimelineMediaPreviewValue(MediaPreviewValue.Private))
}
with(awaitItem()) {
assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Private)
}
}
assertThat(mediaPreviewStore.getSetTimelineMediaPreviewValueEvents()).isEqualTo(
listOf(MediaPreviewValue.Off, MediaPreviewValue.Private)
)
}
@Test
fun `present - media preview state with custom initial values`() = runTest {
val mediaPreviewStore = FakeMediaPreviewConfigStateStore(
hideInviteAvatarsValue = true,
timelineMediaPreviewValue = MediaPreviewValue.Private
)
createModerationAndSafetyPresenter(mediaPreviewConfigStateStore = mediaPreviewStore).test {
with(awaitItem()) {
assertThat(mediaPreviewConfigState.hideInviteAvatars).isTrue()
assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Private)
}
}
}
@Test
fun `present - async actions state`() = runTest {
val mediaPreviewStore = FakeMediaPreviewConfigStateStore(
setHideInviteAvatarsActionValue = AsyncAction.Loading,
setTimelineMediaPreviewActionValue = AsyncAction.Success(Unit)
)
createModerationAndSafetyPresenter(mediaPreviewConfigStateStore = mediaPreviewStore).test {
with(awaitItem()) {
assertThat(mediaPreviewConfigState.setHideInviteAvatarsAction).isEqualTo(AsyncAction.Loading)
assertThat(mediaPreviewConfigState.setTimelineMediaPreviewAction).isEqualTo(AsyncAction.Success(Unit))
}
}
}
private fun CoroutineScope.createModerationAndSafetyPresenter(
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
mediaPreviewConfigStateStore: MediaPreviewConfigStateStore = FakeMediaPreviewConfigStateStore(),
) = ModerationAndSafetyPresenter(
sessionPreferencesStore = sessionPreferencesStore,
mediaPreviewConfigStateStore = mediaPreviewConfigStateStore,
sessionCoroutineScope = this,
)
}
@@ -0,0 +1,153 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.preferences.impl.moderation
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
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 io.element.android.tests.testutils.robolectric.RobolectricTest
import org.junit.Test
import org.robolectric.annotation.Config
class ModerationAndSafetyViewTest : RobolectricTest() {
@Test
fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ModerationAndSafetyEvent>(expectEvents = false)
ensureCalledOnce {
setModerationAndSafetyView(
state = aModerationAndSafetyState(
eventSink = eventsRecorder
),
onBackClick = it
)
pressBack()
}
}
@Test
fun `clicking on Share presence emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ModerationAndSafetyEvent>()
setModerationAndSafetyView(
state = aModerationAndSafetyState(
eventSink = eventsRecorder,
),
)
clickOn(R.string.screen_advanced_settings_share_presence)
eventsRecorder.assertSingle(ModerationAndSafetyEvent.SetSharePresenceEnabled(true))
}
@Test
@Config(qualifiers = "h1080dp")
fun `clicking on hide invite avatars emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ModerationAndSafetyEvent>()
setModerationAndSafetyView(
state = aModerationAndSafetyState(
eventSink = eventsRecorder,
hideInviteAvatars = false
),
)
clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title)
eventsRecorder.assertSingle(ModerationAndSafetyEvent.SetHideInviteAvatars(true))
}
@Test
@Config(qualifiers = "h1080dp")
fun `clicking on timeline media preview always hide emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ModerationAndSafetyEvent>()
setModerationAndSafetyView(
state = aModerationAndSafetyState(
eventSink = eventsRecorder,
timelineMediaPreviewValue = MediaPreviewValue.On
),
)
clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide)
eventsRecorder.assertSingle(ModerationAndSafetyEvent.SetTimelineMediaPreviewValue(MediaPreviewValue.Off))
}
@Test
@Config(qualifiers = "h1080dp")
fun `clicking on timeline media preview private rooms emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ModerationAndSafetyEvent>()
setModerationAndSafetyView(
state = aModerationAndSafetyState(
eventSink = eventsRecorder,
timelineMediaPreviewValue = MediaPreviewValue.On
),
)
clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms)
eventsRecorder.assertSingle(ModerationAndSafetyEvent.SetTimelineMediaPreviewValue(MediaPreviewValue.Private))
}
@Test
@Config(qualifiers = "h1080dp")
fun `clicking on timeline media preview always show emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ModerationAndSafetyEvent>()
setModerationAndSafetyView(
state = aModerationAndSafetyState(
eventSink = eventsRecorder,
timelineMediaPreviewValue = MediaPreviewValue.Off
),
)
clickOn(R.string.screen_advanced_settings_show_media_timeline_always_show)
eventsRecorder.assertSingle(ModerationAndSafetyEvent.SetTimelineMediaPreviewValue(MediaPreviewValue.On))
}
@Test
@Config(qualifiers = "h1080dp")
fun `hide invite avatars toggle is disabled when action is loading`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ModerationAndSafetyEvent>(expectEvents = false)
setModerationAndSafetyView(
state = aModerationAndSafetyState(
eventSink = eventsRecorder,
hideInviteAvatars = false,
setHideInviteAvatarsAction = AsyncAction.Loading
),
)
// The toggle should be disabled, so clicking should not emit any events
clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title)
}
@Test
@Config(qualifiers = "h1080dp")
fun `timeline media preview options are disabled when action is loading`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ModerationAndSafetyEvent>(expectEvents = false)
setModerationAndSafetyView(
state = aModerationAndSafetyState(
eventSink = eventsRecorder,
timelineMediaPreviewValue = MediaPreviewValue.On,
setTimelineMediaPreviewAction = AsyncAction.Loading
),
)
// The options should be disabled, so clicking should not emit any events
clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide)
clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms)
}
}
private fun AndroidComposeUiTest<ComponentActivity>.setModerationAndSafetyView(
state: ModerationAndSafetyState,
onBackClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
ModerationAndSafetyView(
state = state,
onBackClick = onBackClick,
)
}
}
@@ -12,31 +12,22 @@ package io.element.android.features.preferences.impl.root
import app.cash.turbine.ReceiveTurbine
import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.enterprise.test.FakeSessionEnterpriseService
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.compound.theme.Theme
import io.element.android.features.preferences.impl.userstatus.aUserStatusState
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeature
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.indicator.api.IndicatorService
import io.element.android.libraries.indicator.test.FakeIndicatorService
import io.element.android.libraries.matrix.api.oauth.AccountManagementAction
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
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_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.libraries.sessionstorage.api.SessionStore
@@ -48,11 +39,10 @@ import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@@ -63,19 +53,8 @@ class PreferencesRootPresenterTest {
@Test
fun `present - initial state`() = runTest {
val accountManagementUrlResult = lambdaRecorder<AccountManagementAction?, Result<String?>> { action ->
Result.success("$action url")
}
val matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = accountManagementUrlResult,
)
createPresenter(
matrixClient = matrixClient,
sessionEnterpriseService = FakeSessionEnterpriseService(
tweakMasUrlResult = { "tweaked $it" },
),
).test {
val matrixClient = FakeMatrixClient()
createPreferencesRootPresenter(matrixClient = matrixClient).test {
val initialState = awaitItem()
assertThat(initialState.myUser).isEqualTo(
MatrixUser(
@@ -88,106 +67,82 @@ class PreferencesRootPresenterTest {
assertThat(initialState.isMultiAccountEnabled).isFalse()
assertThat(initialState.isOtherAccountsSectionExpanded).isTrue()
assertThat(initialState.otherSessions).isEmpty()
assertThat(initialState.version).isEqualTo("A Version")
val loadedState = awaitItem()
assertThat(loadedState.myUser).isEqualTo(
MatrixUser(
userId = matrixClient.sessionId,
displayName = A_USER_NAME,
avatarUrl = AN_AVATAR_URL
)
assertThat(loadedState.theme).isEqualTo(ThemeOption.System)
assertThat(loadedState.availableThemeOptions).isEqualTo(
listOf(ThemeOption.System, ThemeOption.Light, ThemeOption.Dark).toImmutableList()
)
assertThat(loadedState.showSecureBackup).isFalse()
assertThat(loadedState.showSecureBackupBadge).isFalse()
assertThat(loadedState.accountManagementUrl).isNull()
assertThat(loadedState.userStatusState).isNotNull()
assertThat(loadedState.showAnalyticsSettings).isFalse()
assertThat(loadedState.showLinkNewDevice).isFalse()
assertThat(loadedState.showDeveloperSettings).isTrue()
assertThat(loadedState.canDeactivateAccount).isTrue()
assertThat(loadedState.canReportBug).isTrue()
assertThat(loadedState.nbOfBlockedUsers).isEqualTo(0)
assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState())
assertThat(loadedState.showLabsItem).isFalse()
assertThat(loadedState.snackbarMessage).isNull()
val finalState = awaitItem()
accountManagementUrlResult.assertions().isCalledOnce()
.with(value(null))
assertThat(finalState.accountManagementUrl).isEqualTo("tweaked null url")
}
}
@Test
fun `present - cannot report bug`() = runTest {
val matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success("") },
)
createPresenter(
matrixClient = matrixClient,
rageshakeFeatureAvailability = { flowOf(false) },
fun `present - user status state is null when the server does not support it`() = runTest {
createPreferencesRootPresenter(
matrixClient = FakeMatrixClient().apply {
isUserStatusSupportedResult = Result.success(false)
},
).test {
val initialState = awaitItem()
assertThat(initialState.canReportBug).isFalse()
consumeItemsUntilPredicate { it.userStatusState == null }
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - change theme`() = runTest {
createPreferencesRootPresenter().test {
// Skip until the initial data is loaded
skipItems(1)
with(awaitItem()) {
assertThat(theme).isEqualTo(ThemeOption.System)
eventSink(PreferencesRootEvent.SetTheme(ThemeOption.Dark))
}
with(awaitItem()) {
assertThat(theme).isEqualTo(ThemeOption.Dark)
eventSink(PreferencesRootEvent.SetTheme(ThemeOption.Light))
}
with(awaitItem()) {
assertThat(theme).isEqualTo(ThemeOption.Light)
eventSink(PreferencesRootEvent.SetTheme(ThemeOption.System))
}
with(awaitItem()) {
assertThat(theme).isEqualTo(ThemeOption.System)
}
}
}
@Test
fun `present - number of blocked users`() = runTest {
val matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success("") },
ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID, A_USER_ID_2)),
)
createPresenter(
matrixClient = matrixClient,
fun `present - black theme option shown when feature flag enabled`() = runTest {
createPreferencesRootPresenter(
featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.AllowBlackTheme, true)
}
).test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.nbOfBlockedUsers).isEqualTo(2)
val state = consumeItemsUntilPredicate { it.availableThemeOptions.contains(ThemeOption.Black) }.last()
assertThat(state.availableThemeOptions).isEqualTo(ThemeOption.entries.toImmutableList())
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - secure backup badge`() = runTest {
val matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success("") },
)
val indicatorService = FakeIndicatorService()
createPresenter(
matrixClient = matrixClient,
rageshakeFeatureAvailability = { flowOf(false) },
indicatorService = indicatorService,
).test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.showSecureBackupBadge).isFalse()
indicatorService.setShowSettingChatBackupIndicator(true)
val finalState = awaitItem()
assertThat(finalState.showSecureBackupBadge).isTrue()
fun `present - stored black theme falls back to dark when feature flag disabled`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore().apply {
setTheme(Theme.Black.name)
}
}
@Test
fun `present - can deactivate account is false if the Matrix client say so`() = runTest {
createPresenter(
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { false },
accountManagementUrlResult = { Result.success(null) },
),
).test {
val loadedState = awaitFirstItem()
assertThat(loadedState.canDeactivateAccount).isFalse()
createPreferencesRootPresenter(appPreferencesStore = appPreferencesStore).test {
consumeItemsUntilPredicate { it.theme == ThemeOption.Dark }
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - developer settings is hidden by default in release builds`() = runTest {
createPresenter(
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success(null) },
),
createPreferencesRootPresenter(
showDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.RELEASE))
).test {
val loadedState = awaitFirstItem()
@@ -197,11 +152,7 @@ class PreferencesRootPresenterTest {
@Test
fun `present - developer settings can be enabled in release builds`() = runTest {
createPresenter(
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success(null) },
),
createPreferencesRootPresenter(
showDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.RELEASE))
).test {
val loadedState = awaitFirstItem()
@@ -223,13 +174,7 @@ class PreferencesRootPresenterTest {
),
setLatestSessionResult = setLatestSessionResult,
)
createPresenter(
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success(null) },
),
sessionStore = sessionStore,
).test {
createPreferencesRootPresenter(sessionStore = sessionStore).test {
val loadedState = awaitFirstItem()
loadedState.eventSink(PreferencesRootEvent.SwitchToSession(A_SESSION_ID_2))
setLatestSessionResult.assertions().isCalledOnce()
@@ -239,7 +184,7 @@ class PreferencesRootPresenterTest {
@Test
fun `present - labs can be shown if any feature flag is in labs and not finished`() = runTest {
createPresenter(
createPreferencesRootPresenter(
featureFlagService = FakeFeatureFlagService(
getAvailableFeaturesResult = { _, _ ->
listOf(
@@ -252,10 +197,6 @@ class PreferencesRootPresenterTest {
)
}
),
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success(null) },
),
).test {
assertThat(awaitItem().showLabsItem).isTrue()
cancelAndIgnoreRemainingEvents()
@@ -264,16 +205,12 @@ class PreferencesRootPresenterTest {
@Test
fun `present - labs can't be shown if all feature flags in labs are finished`() = runTest {
createPresenter(
createPreferencesRootPresenter(
featureFlagService = FakeFeatureFlagService(
getAvailableFeaturesResult = { _, _ ->
emptyList()
}
),
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success(null) },
),
).test {
skipItems(1)
assertThat(awaitItem().showLabsItem).isFalse()
@@ -283,11 +220,8 @@ class PreferencesRootPresenterTest {
@Test
fun `present - multiple accounts`() = runTest {
createPresenter(
matrixClient = FakeMatrixClient(
sessionId = A_SESSION_ID,
canDeactivateAccountResult = { true },
),
createPreferencesRootPresenter(
matrixClient = FakeMatrixClient(sessionId = A_SESSION_ID),
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.MultiAccount.key to true)
),
@@ -311,11 +245,7 @@ class PreferencesRootPresenterTest {
@Test
fun `present - other accounts section is collapsed if the preference says so`() = runTest {
createPresenter(
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success(null) },
),
createPreferencesRootPresenter(
appPreferencesStore = InMemoryAppPreferencesStore(isOtherAccountsExpanded = false),
).test {
consumeItemsUntilPredicate { !it.isOtherAccountsSectionExpanded }
@@ -326,13 +256,7 @@ class PreferencesRootPresenterTest {
@Test
fun `present - toggle other accounts expanded stores the new value`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore()
createPresenter(
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success(null) },
),
appPreferencesStore = appPreferencesStore,
).test {
createPreferencesRootPresenter(appPreferencesStore = appPreferencesStore).test {
val initialState = awaitFirstItem()
assertThat(initialState.isOtherAccountsSectionExpanded).isTrue()
initialState.eventSink(PreferencesRootEvent.ToggleOtherAccountsExpanded)
@@ -346,51 +270,27 @@ class PreferencesRootPresenterTest {
}
}
@Test
fun `present - link new device`() = runTest {
createPresenter(
matrixClient = FakeMatrixClient(
sessionId = A_SESSION_ID,
canDeactivateAccountResult = { true },
),
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.QrCodeLogin.key to true)
),
).test {
val state = awaitFirstItem()
assertThat(state.showLinkNewDevice).isTrue()
}
}
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
skipItems(1)
return awaitItem()
}
private fun createPresenter(
private fun CoroutineScope.createPreferencesRootPresenter(
matrixClient: FakeMatrixClient = FakeMatrixClient(),
sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
showDeveloperSettingsProvider: ShowDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.DEBUG)),
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { flowOf(true) },
indicatorService: IndicatorService = FakeIndicatorService(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
sessionStore: SessionStore = InMemorySessionStore(),
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
sessionEnterpriseService: SessionEnterpriseService = FakeSessionEnterpriseService(),
) = PreferencesRootPresenter(
matrixClient = matrixClient,
sessionVerificationService = sessionVerificationService,
analyticsService = FakeAnalyticsService(),
versionFormatter = FakeVersionFormatter(),
snackbarDispatcher = SnackbarDispatcher(),
indicatorService = indicatorService,
directLogoutPresenter = { aDirectLogoutState() },
showDeveloperSettingsProvider = showDeveloperSettingsProvider,
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
featureFlagService = featureFlagService,
sessionStore = sessionStore,
appPreferencesStore = appPreferencesStore,
sessionEnterpriseService = sessionEnterpriseService,
userStatusPresenter = { aUserStatusState() },
sessionCoroutineScope = this,
)
}
@@ -20,20 +20,18 @@ import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.assertNoNodeWithText
import io.element.android.tests.testutils.assertNodeWithTextIsDisplayed
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.robolectric.RobolectricTest
import kotlinx.collections.immutable.toImmutableList
import org.junit.Test
class PreferencesRootViewTest : RobolectricTest() {
@@ -52,16 +50,15 @@ class PreferencesRootViewTest : RobolectricTest() {
}
@Test
fun `click on User profile invokes the expected callback`() = runAndroidComposeUiTest {
fun `click on the user row invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
val user = aMatrixUser()
ensureCalledOnceWithParam(user) { callback ->
ensureCalledOnce { callback ->
setView(
aPreferencesRootState(
myUser = user,
myUser = aMatrixUser(),
eventSink = eventsRecorder,
),
onOpenUserProfile = callback,
onOpenAccountSettings = callback,
)
onNodeWithText("Alice").performClick()
}
@@ -178,84 +175,85 @@ class PreferencesRootViewTest : RobolectricTest() {
}
@Test
fun `click on Encryption invokes the expected callback`() = runAndroidComposeUiTest {
fun `clicking on another theme emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>()
setView(
aPreferencesRootState(
eventSink = eventsRecorder,
),
)
clickOn(CommonStrings.common_appearance)
clickOn(R.string.theme_dark)
eventsRecorder.assertSingle(PreferencesRootEvent.SetTheme(ThemeOption.Dark))
}
@Test
fun `black theme is shown when available`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
setView(
aPreferencesRootState(
availableThemeOptions = ThemeOption.entries.toImmutableList(),
eventSink = eventsRecorder,
),
)
clickOn(CommonStrings.common_appearance)
onNodeWithText(activity!!.getString(R.string.theme_black)).assertExists()
}
@Test
fun `black theme is hidden when unavailable`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
setView(
aPreferencesRootState(
availableThemeOptions = ThemeOption.entries.filterNot { it == ThemeOption.Black }.toImmutableList(),
eventSink = eventsRecorder,
),
)
clickOn(CommonStrings.common_appearance)
assertNoNodeWithText(R.string.theme_black)
}
@Test
fun `click on Media upload quality invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback ->
setView(
aPreferencesRootState(
showSecureBackup = true,
eventSink = eventsRecorder,
),
onSecureBackupClick = callback,
onOpenMediaSettings = callback,
)
clickOn(CommonStrings.common_encryption)
clickOn(CommonStrings.common_media_upload_quality)
}
}
@Test
fun `when showSecureBackup is false, item is not shown`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
setView(
aPreferencesRootState(
showSecureBackup = false,
eventSink = eventsRecorder,
),
)
onNodeWithText(activity!!.getString(CommonStrings.common_encryption)).assertDoesNotExist()
}
@Test
fun `click on Manage account invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnceWithParam("aUrl") { callback ->
setView(
aPreferencesRootState(
accountManagementUrl = "aUrl",
eventSink = eventsRecorder,
),
onManageAccountClick = callback,
)
clickOn(CommonStrings.action_manage_account_and_devices)
}
}
@Test
fun `when accountManagementUrl is null, item is not shown`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
setView(
aPreferencesRootState(
accountManagementUrl = null,
eventSink = eventsRecorder,
),
)
onNodeWithText(activity!!.getString(CommonStrings.action_manage_account_and_devices)).assertDoesNotExist()
}
@Test
fun `click on Link new devices invokes the expected callback`() = runAndroidComposeUiTest {
fun `click on Screen lock invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback ->
setView(
aPreferencesRootState(
showLinkNewDevice = true,
eventSink = eventsRecorder,
),
onLinkNewDeviceClick = callback,
onOpenLockScreenSettings = callback,
)
clickOn(CommonStrings.common_link_new_device)
clickOn(CommonStrings.common_screen_lock)
}
}
@Test
fun `when showLinkNewDevice is false, item is not shown`() = runAndroidComposeUiTest {
fun `click on Location sharing invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
setView(
aPreferencesRootState(
showLinkNewDevice = false,
eventSink = eventsRecorder,
),
)
onNodeWithText(activity!!.getString(CommonStrings.common_link_new_device)).assertDoesNotExist()
ensureCalledOnce { callback ->
setView(
aPreferencesRootState(
eventSink = eventsRecorder,
),
onOpenLocationSettings = callback,
)
val text = activity!!.getString(CommonStrings.common_location_sharing)
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
}
}
@Test
@@ -287,45 +285,31 @@ class PreferencesRootViewTest : RobolectricTest() {
}
@Test
fun `click on Report a problem invokes the expected callback`() = runAndroidComposeUiTest {
fun `click on Labs invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback ->
setView(
aPreferencesRootState(
canReportBug = true,
showLabsItem = true,
eventSink = eventsRecorder,
),
onOpenRageShake = callback,
onOpenLabs = callback,
)
val text = activity!!.getString(CommonStrings.common_report_a_problem)
val text = activity!!.getString(R.string.screen_labs_title)
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
}
}
@Test
fun `when canReportBug is false, item is not shown`() = runAndroidComposeUiTest {
fun `when showLabsItem is false, item is not shown`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
setView(
aPreferencesRootState(
canReportBug = false,
showLabsItem = false,
eventSink = eventsRecorder,
),
)
onNodeWithText(activity!!.getString(CommonStrings.common_report_a_problem)).assertDoesNotExist()
}
@Test
fun `click on Screen lock invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback ->
setView(
aPreferencesRootState(
eventSink = eventsRecorder,
),
onOpenLockScreenSettings = callback,
)
clickOn(CommonStrings.common_screen_lock)
}
onNodeWithText(activity!!.getString(R.string.screen_labs_title)).assertDoesNotExist()
}
@Test
@@ -338,7 +322,8 @@ class PreferencesRootViewTest : RobolectricTest() {
),
onOpenAbout = callback,
)
clickOn(CommonStrings.common_about)
val text = activity!!.getString(CommonStrings.common_about)
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
}
}
@@ -371,128 +356,15 @@ class PreferencesRootViewTest : RobolectricTest() {
}
@Test
fun `click on Advanced settings invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback ->
setView(
aPreferencesRootState(
eventSink = eventsRecorder,
),
onOpenAdvancedSettings = callback,
)
clickOn(CommonStrings.common_advanced_settings)
}
}
@Test
fun `click on Labs invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback ->
setView(
aPreferencesRootState(
showLabsItem = true,
eventSink = eventsRecorder,
),
onOpenLabs = callback,
)
clickOn(R.string.screen_labs_title)
}
}
@Test
fun `when showLabsItem is false, item is not shown`() = runAndroidComposeUiTest {
fun `when userStatusState is null, the status section is not shown`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
setView(
aPreferencesRootState(
showLabsItem = false,
userStatusState = null,
eventSink = eventsRecorder,
),
)
onNodeWithText(activity!!.getString(R.string.screen_labs_title)).assertDoesNotExist()
}
@Test
fun `click on Notification invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback ->
setView(
aPreferencesRootState(
eventSink = eventsRecorder,
),
onOpenNotificationSettings = callback,
)
clickOn(R.string.screen_notification_settings_title)
}
}
@Test
fun `click on Blocked users invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback ->
setView(
aPreferencesRootState(
nbOfBlockedUsers = 1,
eventSink = eventsRecorder,
),
onOpenBlockedUsers = callback,
)
clickOn(CommonStrings.common_blocked_users)
}
}
@Test
fun `when nbOfBlockedUsers is 0, item is not shown`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
setView(
aPreferencesRootState(
nbOfBlockedUsers = 0,
eventSink = eventsRecorder,
),
)
onNodeWithText(activity!!.getString(CommonStrings.common_blocked_users)).assertDoesNotExist()
}
@Test
fun `click on Remove this device invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback ->
setView(
aPreferencesRootState(
eventSink = eventsRecorder,
),
onSignOutClick = callback,
)
val text = activity!!.getString(CommonStrings.action_signout)
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
}
}
@Test
fun `click on Deactivate invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback ->
setView(
aPreferencesRootState(
canDeactivateAccount = true,
eventSink = eventsRecorder,
),
onDeactivateClick = callback,
)
val text = activity!!.getString(CommonStrings.action_delete_account)
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
}
}
@Test
fun `when canDeactivateAccount is false, item is not shown`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
setView(
aPreferencesRootState(
canDeactivateAccount = false,
eventSink = eventsRecorder,
),
)
onNodeWithText(activity!!.getString(CommonStrings.action_delete_account)).assertDoesNotExist()
assertNoNodeWithText(R.string.screen_settings_user_status_placeholder)
}
@Test
@@ -514,21 +386,14 @@ private fun AndroidComposeUiTest<ComponentActivity>.setView(
state: PreferencesRootState,
onBackClick: () -> Unit = EnsureNeverCalled(),
onAddAccountClick: () -> Unit = EnsureNeverCalled(),
onSecureBackupClick: () -> Unit = EnsureNeverCalled(),
onManageAccountClick: (url: String) -> Unit = EnsureNeverCalledWithParam(),
onLinkNewDeviceClick: () -> Unit = EnsureNeverCalled(),
onOpenAnalytics: () -> Unit = EnsureNeverCalled(),
onOpenRageShake: () -> Unit = EnsureNeverCalled(),
onOpenLockScreenSettings: () -> Unit = EnsureNeverCalled(),
onOpenAbout: () -> Unit = EnsureNeverCalled(),
onOpenDeveloperSettings: () -> Unit = EnsureNeverCalled(),
onOpenAdvancedSettings: () -> Unit = EnsureNeverCalled(),
onOpenMediaSettings: () -> Unit = EnsureNeverCalled(),
onOpenLocationSettings: () -> Unit = EnsureNeverCalled(),
onOpenLabs: () -> Unit = EnsureNeverCalled(),
onOpenNotificationSettings: () -> Unit = EnsureNeverCalled(),
onOpenUserProfile: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(),
onOpenBlockedUsers: () -> Unit = EnsureNeverCalled(),
onSignOutClick: () -> Unit = EnsureNeverCalled(),
onDeactivateClick: () -> Unit = EnsureNeverCalled(),
onOpenAccountSettings: () -> Unit = EnsureNeverCalled(),
) {
setContent {
PreferencesRootView(
@@ -536,21 +401,14 @@ private fun AndroidComposeUiTest<ComponentActivity>.setView(
emojiPickerRenderer = NoOpEmojiPickerRenderer,
onBackClick = onBackClick,
onAddAccountClick = onAddAccountClick,
onSecureBackupClick = onSecureBackupClick,
onManageAccountClick = onManageAccountClick,
onLinkNewDeviceClick = onLinkNewDeviceClick,
onOpenAnalytics = onOpenAnalytics,
onOpenRageShake = onOpenRageShake,
onOpenLockScreenSettings = onOpenLockScreenSettings,
onOpenAbout = onOpenAbout,
onOpenDeveloperSettings = onOpenDeveloperSettings,
onOpenMediaSettings = onOpenAdvancedSettings,
onOpenMediaSettings = onOpenMediaSettings,
onOpenLocationSettings = onOpenLocationSettings,
onOpenLabs = onOpenLabs,
onOpenNotificationSettings = onOpenNotificationSettings,
onOpenUserProfile = onOpenUserProfile,
onOpenBlockedUsers = onOpenBlockedUsers,
onSignOutClick = onSignOutClick,
onDeactivateClick = onDeactivateClick,
onOpenAccountSettings = onOpenAccountSettings,
)
}
}