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") @Config(qualifiers = "h1500dp")
@Test @Test
fun `clicking on element call url open the dialogs and submit emits the expected event`() = runAndroidComposeUiTest { fun `clicking on element call url open the dialogs and submit emits the expected event`() = runAndroidComposeUiTest {
@@ -47,6 +47,7 @@ class AppDeveloperSettingsPresenterTest {
) )
presenter.test { presenter.test {
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.isDeveloperModeEnabled).isFalse()
assertThat(state.features).isEmpty() assertThat(state.features).isEmpty()
assertThat(state.customElementCallBaseUrlState).isNotNull() assertThat(state.customElementCallBaseUrlState).isNotNull()
assertThat(state.customElementCallBaseUrlState.baseUrl).isNull() 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( private fun createAppDeveloperSettingsPresenter(
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService( featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(
getAvailableFeaturesResult = { _, _ -> 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. * 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.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details. * 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.RecompositionMode
import app.cash.molecule.moleculeFlow 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 app.cash.turbine.ReceiveTurbine
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.compound.theme.Theme
import io.element.android.features.enterprise.test.FakeSessionEnterpriseService
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.features.preferences.impl.userstatus.aUserStatusState import io.element.android.features.preferences.impl.userstatus.aUserStatusState
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider 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.core.meta.BuildType
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeature import io.element.android.libraries.featureflag.test.FakeFeature
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService 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.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_SESSION_ID 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_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.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.core.aBuildMeta 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.api.store.AppPreferencesStore
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.libraries.sessionstorage.api.SessionStore 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.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test 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.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@@ -63,19 +53,8 @@ class PreferencesRootPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val accountManagementUrlResult = lambdaRecorder<AccountManagementAction?, Result<String?>> { action -> val matrixClient = FakeMatrixClient()
Result.success("$action url") createPreferencesRootPresenter(matrixClient = matrixClient).test {
}
val matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = accountManagementUrlResult,
)
createPresenter(
matrixClient = matrixClient,
sessionEnterpriseService = FakeSessionEnterpriseService(
tweakMasUrlResult = { "tweaked $it" },
),
).test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.myUser).isEqualTo( assertThat(initialState.myUser).isEqualTo(
MatrixUser( MatrixUser(
@@ -88,106 +67,82 @@ class PreferencesRootPresenterTest {
assertThat(initialState.isMultiAccountEnabled).isFalse() assertThat(initialState.isMultiAccountEnabled).isFalse()
assertThat(initialState.isOtherAccountsSectionExpanded).isTrue() assertThat(initialState.isOtherAccountsSectionExpanded).isTrue()
assertThat(initialState.otherSessions).isEmpty() assertThat(initialState.otherSessions).isEmpty()
assertThat(initialState.version).isEqualTo("A Version")
val loadedState = awaitItem() val loadedState = awaitItem()
assertThat(loadedState.myUser).isEqualTo( assertThat(loadedState.theme).isEqualTo(ThemeOption.System)
MatrixUser( assertThat(loadedState.availableThemeOptions).isEqualTo(
userId = matrixClient.sessionId, listOf(ThemeOption.System, ThemeOption.Light, ThemeOption.Dark).toImmutableList()
displayName = A_USER_NAME,
avatarUrl = AN_AVATAR_URL
)
) )
assertThat(loadedState.showSecureBackup).isFalse() assertThat(loadedState.userStatusState).isNotNull()
assertThat(loadedState.showSecureBackupBadge).isFalse()
assertThat(loadedState.accountManagementUrl).isNull()
assertThat(loadedState.showAnalyticsSettings).isFalse() assertThat(loadedState.showAnalyticsSettings).isFalse()
assertThat(loadedState.showLinkNewDevice).isFalse()
assertThat(loadedState.showDeveloperSettings).isTrue() assertThat(loadedState.showDeveloperSettings).isTrue()
assertThat(loadedState.canDeactivateAccount).isTrue() assertThat(loadedState.showLabsItem).isFalse()
assertThat(loadedState.canReportBug).isTrue()
assertThat(loadedState.nbOfBlockedUsers).isEqualTo(0)
assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState())
assertThat(loadedState.snackbarMessage).isNull() assertThat(loadedState.snackbarMessage).isNull()
val finalState = awaitItem()
accountManagementUrlResult.assertions().isCalledOnce()
.with(value(null))
assertThat(finalState.accountManagementUrl).isEqualTo("tweaked null url")
} }
} }
@Test @Test
fun `present - cannot report bug`() = runTest { fun `present - user status state is null when the server does not support it`() = runTest {
val matrixClient = FakeMatrixClient( createPreferencesRootPresenter(
canDeactivateAccountResult = { true }, matrixClient = FakeMatrixClient().apply {
accountManagementUrlResult = { Result.success("") }, isUserStatusSupportedResult = Result.success(false)
) },
createPresenter(
matrixClient = matrixClient,
rageshakeFeatureAvailability = { flowOf(false) },
).test { ).test {
val initialState = awaitItem() consumeItemsUntilPredicate { it.userStatusState == null }
assertThat(initialState.canReportBug).isFalse() cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - change theme`() = runTest {
createPreferencesRootPresenter().test {
// Skip until the initial data is loaded
skipItems(1) 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 @Test
fun `present - number of blocked users`() = runTest { fun `present - black theme option shown when feature flag enabled`() = runTest {
val matrixClient = FakeMatrixClient( createPreferencesRootPresenter(
canDeactivateAccountResult = { true }, featureFlagService = FakeFeatureFlagService().apply {
accountManagementUrlResult = { Result.success("") }, setFeatureEnabled(FeatureFlags.AllowBlackTheme, true)
ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID, A_USER_ID_2)), }
)
createPresenter(
matrixClient = matrixClient,
).test { ).test {
skipItems(1) val state = consumeItemsUntilPredicate { it.availableThemeOptions.contains(ThemeOption.Black) }.last()
val initialState = awaitItem() assertThat(state.availableThemeOptions).isEqualTo(ThemeOption.entries.toImmutableList())
assertThat(initialState.nbOfBlockedUsers).isEqualTo(2) cancelAndIgnoreRemainingEvents()
} }
} }
@Test @Test
fun `present - secure backup badge`() = runTest { fun `present - stored black theme falls back to dark when feature flag disabled`() = runTest {
val matrixClient = FakeMatrixClient( val appPreferencesStore = InMemoryAppPreferencesStore().apply {
canDeactivateAccountResult = { true }, setTheme(Theme.Black.name)
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()
} }
} createPreferencesRootPresenter(appPreferencesStore = appPreferencesStore).test {
consumeItemsUntilPredicate { it.theme == ThemeOption.Dark }
@Test cancelAndIgnoreRemainingEvents()
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()
} }
} }
@Test @Test
fun `present - developer settings is hidden by default in release builds`() = runTest { fun `present - developer settings is hidden by default in release builds`() = runTest {
createPresenter( createPreferencesRootPresenter(
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success(null) },
),
showDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.RELEASE)) showDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.RELEASE))
).test { ).test {
val loadedState = awaitFirstItem() val loadedState = awaitFirstItem()
@@ -197,11 +152,7 @@ class PreferencesRootPresenterTest {
@Test @Test
fun `present - developer settings can be enabled in release builds`() = runTest { fun `present - developer settings can be enabled in release builds`() = runTest {
createPresenter( createPreferencesRootPresenter(
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success(null) },
),
showDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.RELEASE)) showDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.RELEASE))
).test { ).test {
val loadedState = awaitFirstItem() val loadedState = awaitFirstItem()
@@ -223,13 +174,7 @@ class PreferencesRootPresenterTest {
), ),
setLatestSessionResult = setLatestSessionResult, setLatestSessionResult = setLatestSessionResult,
) )
createPresenter( createPreferencesRootPresenter(sessionStore = sessionStore).test {
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success(null) },
),
sessionStore = sessionStore,
).test {
val loadedState = awaitFirstItem() val loadedState = awaitFirstItem()
loadedState.eventSink(PreferencesRootEvent.SwitchToSession(A_SESSION_ID_2)) loadedState.eventSink(PreferencesRootEvent.SwitchToSession(A_SESSION_ID_2))
setLatestSessionResult.assertions().isCalledOnce() setLatestSessionResult.assertions().isCalledOnce()
@@ -239,7 +184,7 @@ class PreferencesRootPresenterTest {
@Test @Test
fun `present - labs can be shown if any feature flag is in labs and not finished`() = runTest { fun `present - labs can be shown if any feature flag is in labs and not finished`() = runTest {
createPresenter( createPreferencesRootPresenter(
featureFlagService = FakeFeatureFlagService( featureFlagService = FakeFeatureFlagService(
getAvailableFeaturesResult = { _, _ -> getAvailableFeaturesResult = { _, _ ->
listOf( listOf(
@@ -252,10 +197,6 @@ class PreferencesRootPresenterTest {
) )
} }
), ),
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success(null) },
),
).test { ).test {
assertThat(awaitItem().showLabsItem).isTrue() assertThat(awaitItem().showLabsItem).isTrue()
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
@@ -264,16 +205,12 @@ class PreferencesRootPresenterTest {
@Test @Test
fun `present - labs can't be shown if all feature flags in labs are finished`() = runTest { fun `present - labs can't be shown if all feature flags in labs are finished`() = runTest {
createPresenter( createPreferencesRootPresenter(
featureFlagService = FakeFeatureFlagService( featureFlagService = FakeFeatureFlagService(
getAvailableFeaturesResult = { _, _ -> getAvailableFeaturesResult = { _, _ ->
emptyList() emptyList()
} }
), ),
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success(null) },
),
).test { ).test {
skipItems(1) skipItems(1)
assertThat(awaitItem().showLabsItem).isFalse() assertThat(awaitItem().showLabsItem).isFalse()
@@ -283,11 +220,8 @@ class PreferencesRootPresenterTest {
@Test @Test
fun `present - multiple accounts`() = runTest { fun `present - multiple accounts`() = runTest {
createPresenter( createPreferencesRootPresenter(
matrixClient = FakeMatrixClient( matrixClient = FakeMatrixClient(sessionId = A_SESSION_ID),
sessionId = A_SESSION_ID,
canDeactivateAccountResult = { true },
),
featureFlagService = FakeFeatureFlagService( featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.MultiAccount.key to true) initialState = mapOf(FeatureFlags.MultiAccount.key to true)
), ),
@@ -311,11 +245,7 @@ class PreferencesRootPresenterTest {
@Test @Test
fun `present - other accounts section is collapsed if the preference says so`() = runTest { fun `present - other accounts section is collapsed if the preference says so`() = runTest {
createPresenter( createPreferencesRootPresenter(
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success(null) },
),
appPreferencesStore = InMemoryAppPreferencesStore(isOtherAccountsExpanded = false), appPreferencesStore = InMemoryAppPreferencesStore(isOtherAccountsExpanded = false),
).test { ).test {
consumeItemsUntilPredicate { !it.isOtherAccountsSectionExpanded } consumeItemsUntilPredicate { !it.isOtherAccountsSectionExpanded }
@@ -326,13 +256,7 @@ class PreferencesRootPresenterTest {
@Test @Test
fun `present - toggle other accounts expanded stores the new value`() = runTest { fun `present - toggle other accounts expanded stores the new value`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore() val appPreferencesStore = InMemoryAppPreferencesStore()
createPresenter( createPreferencesRootPresenter(appPreferencesStore = appPreferencesStore).test {
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success(null) },
),
appPreferencesStore = appPreferencesStore,
).test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
assertThat(initialState.isOtherAccountsSectionExpanded).isTrue() assertThat(initialState.isOtherAccountsSectionExpanded).isTrue()
initialState.eventSink(PreferencesRootEvent.ToggleOtherAccountsExpanded) 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 { private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
skipItems(1) skipItems(1)
return awaitItem() return awaitItem()
} }
private fun createPresenter( private fun CoroutineScope.createPreferencesRootPresenter(
matrixClient: FakeMatrixClient = FakeMatrixClient(), matrixClient: FakeMatrixClient = FakeMatrixClient(),
sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
showDeveloperSettingsProvider: ShowDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.DEBUG)), showDeveloperSettingsProvider: ShowDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.DEBUG)),
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { flowOf(true) },
indicatorService: IndicatorService = FakeIndicatorService(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(), featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
sessionStore: SessionStore = InMemorySessionStore(), sessionStore: SessionStore = InMemorySessionStore(),
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
sessionEnterpriseService: SessionEnterpriseService = FakeSessionEnterpriseService(),
) = PreferencesRootPresenter( ) = PreferencesRootPresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
sessionVerificationService = sessionVerificationService,
analyticsService = FakeAnalyticsService(), analyticsService = FakeAnalyticsService(),
versionFormatter = FakeVersionFormatter(), versionFormatter = FakeVersionFormatter(),
snackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher = SnackbarDispatcher(),
indicatorService = indicatorService,
directLogoutPresenter = { aDirectLogoutState() },
showDeveloperSettingsProvider = showDeveloperSettingsProvider, showDeveloperSettingsProvider = showDeveloperSettingsProvider,
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
sessionStore = sessionStore, sessionStore = sessionStore,
appPreferencesStore = appPreferencesStore, appPreferencesStore = appPreferencesStore,
sessionEnterpriseService = sessionEnterpriseService,
userStatusPresenter = { aUserStatusState() }, userStatusPresenter = { aUserStatusState() },
sessionCoroutineScope = this,
) )
} }
@@ -20,20 +20,18 @@ import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer 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.test.A_USER_ID_2
import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.assertNoNodeWithText import io.element.android.tests.testutils.assertNoNodeWithText
import io.element.android.tests.testutils.assertNodeWithTextIsDisplayed import io.element.android.tests.testutils.assertNodeWithTextIsDisplayed
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce 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.pressBack
import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.robolectric.RobolectricTest
import kotlinx.collections.immutable.toImmutableList
import org.junit.Test import org.junit.Test
class PreferencesRootViewTest : RobolectricTest() { class PreferencesRootViewTest : RobolectricTest() {
@@ -52,16 +50,15 @@ class PreferencesRootViewTest : RobolectricTest() {
} }
@Test @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 eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
val user = aMatrixUser() ensureCalledOnce { callback ->
ensureCalledOnceWithParam(user) { callback ->
setView( setView(
aPreferencesRootState( aPreferencesRootState(
myUser = user, myUser = aMatrixUser(),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onOpenUserProfile = callback, onOpenAccountSettings = callback,
) )
onNodeWithText("Alice").performClick() onNodeWithText("Alice").performClick()
} }
@@ -178,84 +175,85 @@ class PreferencesRootViewTest : RobolectricTest() {
} }
@Test @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) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
setView( setView(
aPreferencesRootState( aPreferencesRootState(
showSecureBackup = true,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onSecureBackupClick = callback, onOpenMediaSettings = callback,
) )
clickOn(CommonStrings.common_encryption) clickOn(CommonStrings.common_media_upload_quality)
} }
} }
@Test @Test
fun `when showSecureBackup is false, item is not shown`() = runAndroidComposeUiTest { fun `click on Screen lock invokes the expected callback`() = 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 {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
setView( setView(
aPreferencesRootState( aPreferencesRootState(
showLinkNewDevice = true,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onLinkNewDeviceClick = callback, onOpenLockScreenSettings = callback,
) )
clickOn(CommonStrings.common_link_new_device) clickOn(CommonStrings.common_screen_lock)
} }
} }
@Test @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) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
setView( ensureCalledOnce { callback ->
aPreferencesRootState( setView(
showLinkNewDevice = false, aPreferencesRootState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) onOpenLocationSettings = callback,
onNodeWithText(activity!!.getString(CommonStrings.common_link_new_device)).assertDoesNotExist() )
val text = activity!!.getString(CommonStrings.common_location_sharing)
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
}
} }
@Test @Test
@@ -287,45 +285,31 @@ class PreferencesRootViewTest : RobolectricTest() {
} }
@Test @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) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
setView( setView(
aPreferencesRootState( aPreferencesRootState(
canReportBug = true, showLabsItem = true,
eventSink = eventsRecorder, 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() onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
} }
} }
@Test @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) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
setView( setView(
aPreferencesRootState( aPreferencesRootState(
canReportBug = false, showLabsItem = false,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
onNodeWithText(activity!!.getString(CommonStrings.common_report_a_problem)).assertDoesNotExist() onNodeWithText(activity!!.getString(R.string.screen_labs_title)).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)
}
} }
@Test @Test
@@ -338,7 +322,8 @@ class PreferencesRootViewTest : RobolectricTest() {
), ),
onOpenAbout = callback, 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 @Test
fun `click on Advanced settings invokes the expected callback`() = runAndroidComposeUiTest { fun `when userStatusState is null, the status section is not shown`() = 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 {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
setView( setView(
aPreferencesRootState( aPreferencesRootState(
showLabsItem = false, userStatusState = null,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
onNodeWithText(activity!!.getString(R.string.screen_labs_title)).assertDoesNotExist() assertNoNodeWithText(R.string.screen_settings_user_status_placeholder)
}
@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()
} }
@Test @Test
@@ -514,21 +386,14 @@ private fun AndroidComposeUiTest<ComponentActivity>.setView(
state: PreferencesRootState, state: PreferencesRootState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
onAddAccountClick: () -> Unit = EnsureNeverCalled(), onAddAccountClick: () -> Unit = EnsureNeverCalled(),
onSecureBackupClick: () -> Unit = EnsureNeverCalled(),
onManageAccountClick: (url: String) -> Unit = EnsureNeverCalledWithParam(),
onLinkNewDeviceClick: () -> Unit = EnsureNeverCalled(),
onOpenAnalytics: () -> Unit = EnsureNeverCalled(), onOpenAnalytics: () -> Unit = EnsureNeverCalled(),
onOpenRageShake: () -> Unit = EnsureNeverCalled(),
onOpenLockScreenSettings: () -> Unit = EnsureNeverCalled(), onOpenLockScreenSettings: () -> Unit = EnsureNeverCalled(),
onOpenAbout: () -> Unit = EnsureNeverCalled(), onOpenAbout: () -> Unit = EnsureNeverCalled(),
onOpenDeveloperSettings: () -> Unit = EnsureNeverCalled(), onOpenDeveloperSettings: () -> Unit = EnsureNeverCalled(),
onOpenAdvancedSettings: () -> Unit = EnsureNeverCalled(), onOpenMediaSettings: () -> Unit = EnsureNeverCalled(),
onOpenLocationSettings: () -> Unit = EnsureNeverCalled(),
onOpenLabs: () -> Unit = EnsureNeverCalled(), onOpenLabs: () -> Unit = EnsureNeverCalled(),
onOpenNotificationSettings: () -> Unit = EnsureNeverCalled(), onOpenAccountSettings: () -> Unit = EnsureNeverCalled(),
onOpenUserProfile: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(),
onOpenBlockedUsers: () -> Unit = EnsureNeverCalled(),
onSignOutClick: () -> Unit = EnsureNeverCalled(),
onDeactivateClick: () -> Unit = EnsureNeverCalled(),
) { ) {
setContent { setContent {
PreferencesRootView( PreferencesRootView(
@@ -536,21 +401,14 @@ private fun AndroidComposeUiTest<ComponentActivity>.setView(
emojiPickerRenderer = NoOpEmojiPickerRenderer, emojiPickerRenderer = NoOpEmojiPickerRenderer,
onBackClick = onBackClick, onBackClick = onBackClick,
onAddAccountClick = onAddAccountClick, onAddAccountClick = onAddAccountClick,
onSecureBackupClick = onSecureBackupClick,
onManageAccountClick = onManageAccountClick,
onLinkNewDeviceClick = onLinkNewDeviceClick,
onOpenAnalytics = onOpenAnalytics, onOpenAnalytics = onOpenAnalytics,
onOpenRageShake = onOpenRageShake,
onOpenLockScreenSettings = onOpenLockScreenSettings, onOpenLockScreenSettings = onOpenLockScreenSettings,
onOpenAbout = onOpenAbout, onOpenAbout = onOpenAbout,
onOpenDeveloperSettings = onOpenDeveloperSettings, onOpenDeveloperSettings = onOpenDeveloperSettings,
onOpenMediaSettings = onOpenAdvancedSettings, onOpenMediaSettings = onOpenMediaSettings,
onOpenLocationSettings = onOpenLocationSettings,
onOpenLabs = onOpenLabs, onOpenLabs = onOpenLabs,
onOpenNotificationSettings = onOpenNotificationSettings, onOpenAccountSettings = onOpenAccountSettings,
onOpenUserProfile = onOpenUserProfile,
onOpenBlockedUsers = onOpenBlockedUsers,
onSignOutClick = onSignOutClick,
onDeactivateClick = onDeactivateClick,
) )
} }
} }