mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
Let Claude update the tests.
This commit is contained in:
committed by
Benoit Marty
parent
9b2577f1dd
commit
872bf5a70f
+198
@@ -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,
|
||||
)
|
||||
}
|
||||
+245
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+12
@@ -48,6 +48,18 @@ class AppDeveloperSettingsPageTest : RobolectricTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on View source emits the expected event`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<AppDeveloperSettingsEvent>()
|
||||
setAppDeveloperSettingsView(
|
||||
state = anAppDeveloperSettingsState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
clickOn(CommonStrings.action_view_source)
|
||||
eventsRecorder.assertSingle(AppDeveloperSettingsEvent.SetDeveloperModeEnabled(true))
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1500dp")
|
||||
@Test
|
||||
fun `clicking on element call url open the dialogs and submit emits the expected event`() = runAndroidComposeUiTest {
|
||||
|
||||
+21
@@ -47,6 +47,7 @@ class AppDeveloperSettingsPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.isDeveloperModeEnabled).isFalse()
|
||||
assertThat(state.features).isEmpty()
|
||||
assertThat(state.customElementCallBaseUrlState).isNotNull()
|
||||
assertThat(state.customElementCallBaseUrlState.baseUrl).isNull()
|
||||
@@ -130,6 +131,26 @@ class AppDeveloperSettingsPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - developer mode on off`() = runTest {
|
||||
val preferences = InMemoryAppPreferencesStore()
|
||||
val presenter = createAppDeveloperSettingsPresenter(preferencesStore = preferences)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.isDeveloperModeEnabled).isFalse()
|
||||
state.eventSink(AppDeveloperSettingsEvent.SetDeveloperModeEnabled(true))
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.isDeveloperModeEnabled).isTrue()
|
||||
state.eventSink(AppDeveloperSettingsEvent.SetDeveloperModeEnabled(false))
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.isDeveloperModeEnabled).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAppDeveloperSettingsPresenter(
|
||||
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(
|
||||
getAvailableFeaturesResult = { _, _ ->
|
||||
|
||||
+71
@@ -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,
|
||||
)
|
||||
}
|
||||
+88
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
-393
@@ -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,
|
||||
)
|
||||
}
|
||||
-264
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+114
@@ -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,
|
||||
)
|
||||
}
|
||||
+173
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.preferences.impl.media
|
||||
package io.element.android.features.preferences.impl.moderation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.preferences.impl.media
|
||||
package io.element.android.features.preferences.impl.moderation
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
+131
@@ -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,
|
||||
)
|
||||
}
|
||||
+153
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+65
-165
@@ -12,31 +12,22 @@ package io.element.android.features.preferences.impl.root
|
||||
|
||||
import app.cash.turbine.ReceiveTurbine
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
||||
import io.element.android.features.enterprise.test.FakeSessionEnterpriseService
|
||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.features.preferences.impl.userstatus.aUserStatusState
|
||||
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
|
||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeature
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.indicator.api.IndicatorService
|
||||
import io.element.android.libraries.indicator.test.FakeIndicatorService
|
||||
import io.element.android.libraries.matrix.api.oauth.AccountManagementAction
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
@@ -48,11 +39,10 @@ import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -63,19 +53,8 @@ class PreferencesRootPresenterTest {
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val accountManagementUrlResult = lambdaRecorder<AccountManagementAction?, Result<String?>> { action ->
|
||||
Result.success("$action url")
|
||||
}
|
||||
val matrixClient = FakeMatrixClient(
|
||||
canDeactivateAccountResult = { true },
|
||||
accountManagementUrlResult = accountManagementUrlResult,
|
||||
)
|
||||
createPresenter(
|
||||
matrixClient = matrixClient,
|
||||
sessionEnterpriseService = FakeSessionEnterpriseService(
|
||||
tweakMasUrlResult = { "tweaked $it" },
|
||||
),
|
||||
).test {
|
||||
val matrixClient = FakeMatrixClient()
|
||||
createPreferencesRootPresenter(matrixClient = matrixClient).test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.myUser).isEqualTo(
|
||||
MatrixUser(
|
||||
@@ -88,106 +67,82 @@ class PreferencesRootPresenterTest {
|
||||
assertThat(initialState.isMultiAccountEnabled).isFalse()
|
||||
assertThat(initialState.isOtherAccountsSectionExpanded).isTrue()
|
||||
assertThat(initialState.otherSessions).isEmpty()
|
||||
assertThat(initialState.version).isEqualTo("A Version")
|
||||
val loadedState = awaitItem()
|
||||
assertThat(loadedState.myUser).isEqualTo(
|
||||
MatrixUser(
|
||||
userId = matrixClient.sessionId,
|
||||
displayName = A_USER_NAME,
|
||||
avatarUrl = AN_AVATAR_URL
|
||||
)
|
||||
assertThat(loadedState.theme).isEqualTo(ThemeOption.System)
|
||||
assertThat(loadedState.availableThemeOptions).isEqualTo(
|
||||
listOf(ThemeOption.System, ThemeOption.Light, ThemeOption.Dark).toImmutableList()
|
||||
)
|
||||
assertThat(loadedState.showSecureBackup).isFalse()
|
||||
assertThat(loadedState.showSecureBackupBadge).isFalse()
|
||||
assertThat(loadedState.accountManagementUrl).isNull()
|
||||
assertThat(loadedState.userStatusState).isNotNull()
|
||||
assertThat(loadedState.showAnalyticsSettings).isFalse()
|
||||
assertThat(loadedState.showLinkNewDevice).isFalse()
|
||||
assertThat(loadedState.showDeveloperSettings).isTrue()
|
||||
assertThat(loadedState.canDeactivateAccount).isTrue()
|
||||
assertThat(loadedState.canReportBug).isTrue()
|
||||
assertThat(loadedState.nbOfBlockedUsers).isEqualTo(0)
|
||||
assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState())
|
||||
assertThat(loadedState.showLabsItem).isFalse()
|
||||
assertThat(loadedState.snackbarMessage).isNull()
|
||||
val finalState = awaitItem()
|
||||
accountManagementUrlResult.assertions().isCalledOnce()
|
||||
.with(value(null))
|
||||
assertThat(finalState.accountManagementUrl).isEqualTo("tweaked null url")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - cannot report bug`() = runTest {
|
||||
val matrixClient = FakeMatrixClient(
|
||||
canDeactivateAccountResult = { true },
|
||||
accountManagementUrlResult = { Result.success("") },
|
||||
)
|
||||
createPresenter(
|
||||
matrixClient = matrixClient,
|
||||
rageshakeFeatureAvailability = { flowOf(false) },
|
||||
fun `present - user status state is null when the server does not support it`() = runTest {
|
||||
createPreferencesRootPresenter(
|
||||
matrixClient = FakeMatrixClient().apply {
|
||||
isUserStatusSupportedResult = Result.success(false)
|
||||
},
|
||||
).test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.canReportBug).isFalse()
|
||||
consumeItemsUntilPredicate { it.userStatusState == null }
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - change theme`() = runTest {
|
||||
createPreferencesRootPresenter().test {
|
||||
// Skip until the initial data is loaded
|
||||
skipItems(1)
|
||||
|
||||
with(awaitItem()) {
|
||||
assertThat(theme).isEqualTo(ThemeOption.System)
|
||||
eventSink(PreferencesRootEvent.SetTheme(ThemeOption.Dark))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(theme).isEqualTo(ThemeOption.Dark)
|
||||
eventSink(PreferencesRootEvent.SetTheme(ThemeOption.Light))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(theme).isEqualTo(ThemeOption.Light)
|
||||
eventSink(PreferencesRootEvent.SetTheme(ThemeOption.System))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(theme).isEqualTo(ThemeOption.System)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - number of blocked users`() = runTest {
|
||||
val matrixClient = FakeMatrixClient(
|
||||
canDeactivateAccountResult = { true },
|
||||
accountManagementUrlResult = { Result.success("") },
|
||||
ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID, A_USER_ID_2)),
|
||||
)
|
||||
createPresenter(
|
||||
matrixClient = matrixClient,
|
||||
fun `present - black theme option shown when feature flag enabled`() = runTest {
|
||||
createPreferencesRootPresenter(
|
||||
featureFlagService = FakeFeatureFlagService().apply {
|
||||
setFeatureEnabled(FeatureFlags.AllowBlackTheme, true)
|
||||
}
|
||||
).test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.nbOfBlockedUsers).isEqualTo(2)
|
||||
val state = consumeItemsUntilPredicate { it.availableThemeOptions.contains(ThemeOption.Black) }.last()
|
||||
assertThat(state.availableThemeOptions).isEqualTo(ThemeOption.entries.toImmutableList())
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - secure backup badge`() = runTest {
|
||||
val matrixClient = FakeMatrixClient(
|
||||
canDeactivateAccountResult = { true },
|
||||
accountManagementUrlResult = { Result.success("") },
|
||||
)
|
||||
val indicatorService = FakeIndicatorService()
|
||||
createPresenter(
|
||||
matrixClient = matrixClient,
|
||||
rageshakeFeatureAvailability = { flowOf(false) },
|
||||
indicatorService = indicatorService,
|
||||
).test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.showSecureBackupBadge).isFalse()
|
||||
indicatorService.setShowSettingChatBackupIndicator(true)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.showSecureBackupBadge).isTrue()
|
||||
fun `present - stored black theme falls back to dark when feature flag disabled`() = runTest {
|
||||
val appPreferencesStore = InMemoryAppPreferencesStore().apply {
|
||||
setTheme(Theme.Black.name)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - can deactivate account is false if the Matrix client say so`() = runTest {
|
||||
createPresenter(
|
||||
matrixClient = FakeMatrixClient(
|
||||
canDeactivateAccountResult = { false },
|
||||
accountManagementUrlResult = { Result.success(null) },
|
||||
),
|
||||
).test {
|
||||
val loadedState = awaitFirstItem()
|
||||
assertThat(loadedState.canDeactivateAccount).isFalse()
|
||||
createPreferencesRootPresenter(appPreferencesStore = appPreferencesStore).test {
|
||||
consumeItemsUntilPredicate { it.theme == ThemeOption.Dark }
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - developer settings is hidden by default in release builds`() = runTest {
|
||||
createPresenter(
|
||||
matrixClient = FakeMatrixClient(
|
||||
canDeactivateAccountResult = { true },
|
||||
accountManagementUrlResult = { Result.success(null) },
|
||||
),
|
||||
createPreferencesRootPresenter(
|
||||
showDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.RELEASE))
|
||||
).test {
|
||||
val loadedState = awaitFirstItem()
|
||||
@@ -197,11 +152,7 @@ class PreferencesRootPresenterTest {
|
||||
|
||||
@Test
|
||||
fun `present - developer settings can be enabled in release builds`() = runTest {
|
||||
createPresenter(
|
||||
matrixClient = FakeMatrixClient(
|
||||
canDeactivateAccountResult = { true },
|
||||
accountManagementUrlResult = { Result.success(null) },
|
||||
),
|
||||
createPreferencesRootPresenter(
|
||||
showDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.RELEASE))
|
||||
).test {
|
||||
val loadedState = awaitFirstItem()
|
||||
@@ -223,13 +174,7 @@ class PreferencesRootPresenterTest {
|
||||
),
|
||||
setLatestSessionResult = setLatestSessionResult,
|
||||
)
|
||||
createPresenter(
|
||||
matrixClient = FakeMatrixClient(
|
||||
canDeactivateAccountResult = { true },
|
||||
accountManagementUrlResult = { Result.success(null) },
|
||||
),
|
||||
sessionStore = sessionStore,
|
||||
).test {
|
||||
createPreferencesRootPresenter(sessionStore = sessionStore).test {
|
||||
val loadedState = awaitFirstItem()
|
||||
loadedState.eventSink(PreferencesRootEvent.SwitchToSession(A_SESSION_ID_2))
|
||||
setLatestSessionResult.assertions().isCalledOnce()
|
||||
@@ -239,7 +184,7 @@ class PreferencesRootPresenterTest {
|
||||
|
||||
@Test
|
||||
fun `present - labs can be shown if any feature flag is in labs and not finished`() = runTest {
|
||||
createPresenter(
|
||||
createPreferencesRootPresenter(
|
||||
featureFlagService = FakeFeatureFlagService(
|
||||
getAvailableFeaturesResult = { _, _ ->
|
||||
listOf(
|
||||
@@ -252,10 +197,6 @@ class PreferencesRootPresenterTest {
|
||||
)
|
||||
}
|
||||
),
|
||||
matrixClient = FakeMatrixClient(
|
||||
canDeactivateAccountResult = { true },
|
||||
accountManagementUrlResult = { Result.success(null) },
|
||||
),
|
||||
).test {
|
||||
assertThat(awaitItem().showLabsItem).isTrue()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
@@ -264,16 +205,12 @@ class PreferencesRootPresenterTest {
|
||||
|
||||
@Test
|
||||
fun `present - labs can't be shown if all feature flags in labs are finished`() = runTest {
|
||||
createPresenter(
|
||||
createPreferencesRootPresenter(
|
||||
featureFlagService = FakeFeatureFlagService(
|
||||
getAvailableFeaturesResult = { _, _ ->
|
||||
emptyList()
|
||||
}
|
||||
),
|
||||
matrixClient = FakeMatrixClient(
|
||||
canDeactivateAccountResult = { true },
|
||||
accountManagementUrlResult = { Result.success(null) },
|
||||
),
|
||||
).test {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem().showLabsItem).isFalse()
|
||||
@@ -283,11 +220,8 @@ class PreferencesRootPresenterTest {
|
||||
|
||||
@Test
|
||||
fun `present - multiple accounts`() = runTest {
|
||||
createPresenter(
|
||||
matrixClient = FakeMatrixClient(
|
||||
sessionId = A_SESSION_ID,
|
||||
canDeactivateAccountResult = { true },
|
||||
),
|
||||
createPreferencesRootPresenter(
|
||||
matrixClient = FakeMatrixClient(sessionId = A_SESSION_ID),
|
||||
featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(FeatureFlags.MultiAccount.key to true)
|
||||
),
|
||||
@@ -311,11 +245,7 @@ class PreferencesRootPresenterTest {
|
||||
|
||||
@Test
|
||||
fun `present - other accounts section is collapsed if the preference says so`() = runTest {
|
||||
createPresenter(
|
||||
matrixClient = FakeMatrixClient(
|
||||
canDeactivateAccountResult = { true },
|
||||
accountManagementUrlResult = { Result.success(null) },
|
||||
),
|
||||
createPreferencesRootPresenter(
|
||||
appPreferencesStore = InMemoryAppPreferencesStore(isOtherAccountsExpanded = false),
|
||||
).test {
|
||||
consumeItemsUntilPredicate { !it.isOtherAccountsSectionExpanded }
|
||||
@@ -326,13 +256,7 @@ class PreferencesRootPresenterTest {
|
||||
@Test
|
||||
fun `present - toggle other accounts expanded stores the new value`() = runTest {
|
||||
val appPreferencesStore = InMemoryAppPreferencesStore()
|
||||
createPresenter(
|
||||
matrixClient = FakeMatrixClient(
|
||||
canDeactivateAccountResult = { true },
|
||||
accountManagementUrlResult = { Result.success(null) },
|
||||
),
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
).test {
|
||||
createPreferencesRootPresenter(appPreferencesStore = appPreferencesStore).test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.isOtherAccountsSectionExpanded).isTrue()
|
||||
initialState.eventSink(PreferencesRootEvent.ToggleOtherAccountsExpanded)
|
||||
@@ -346,51 +270,27 @@ class PreferencesRootPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - link new device`() = runTest {
|
||||
createPresenter(
|
||||
matrixClient = FakeMatrixClient(
|
||||
sessionId = A_SESSION_ID,
|
||||
canDeactivateAccountResult = { true },
|
||||
),
|
||||
featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(FeatureFlags.QrCodeLogin.key to true)
|
||||
),
|
||||
).test {
|
||||
val state = awaitFirstItem()
|
||||
assertThat(state.showLinkNewDevice).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
||||
skipItems(1)
|
||||
return awaitItem()
|
||||
}
|
||||
|
||||
private fun createPresenter(
|
||||
private fun CoroutineScope.createPreferencesRootPresenter(
|
||||
matrixClient: FakeMatrixClient = FakeMatrixClient(),
|
||||
sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
|
||||
showDeveloperSettingsProvider: ShowDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.DEBUG)),
|
||||
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { flowOf(true) },
|
||||
indicatorService: IndicatorService = FakeIndicatorService(),
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||
sessionStore: SessionStore = InMemorySessionStore(),
|
||||
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||
sessionEnterpriseService: SessionEnterpriseService = FakeSessionEnterpriseService(),
|
||||
) = PreferencesRootPresenter(
|
||||
matrixClient = matrixClient,
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
versionFormatter = FakeVersionFormatter(),
|
||||
snackbarDispatcher = SnackbarDispatcher(),
|
||||
indicatorService = indicatorService,
|
||||
directLogoutPresenter = { aDirectLogoutState() },
|
||||
showDeveloperSettingsProvider = showDeveloperSettingsProvider,
|
||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||
featureFlagService = featureFlagService,
|
||||
sessionStore = sessionStore,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
sessionEnterpriseService = sessionEnterpriseService,
|
||||
userStatusPresenter = { aUserStatusState() },
|
||||
sessionCoroutineScope = this,
|
||||
)
|
||||
}
|
||||
|
||||
+79
-221
@@ -20,20 +20,18 @@ import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
||||
import io.element.android.features.preferences.impl.R
|
||||
import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.assertNoNodeWithText
|
||||
import io.element.android.tests.testutils.assertNodeWithTextIsDisplayed
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.junit.Test
|
||||
|
||||
class PreferencesRootViewTest : RobolectricTest() {
|
||||
@@ -52,16 +50,15 @@ class PreferencesRootViewTest : RobolectricTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on User profile invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
fun `click on the user row invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
val user = aMatrixUser()
|
||||
ensureCalledOnceWithParam(user) { callback ->
|
||||
ensureCalledOnce { callback ->
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
myUser = user,
|
||||
myUser = aMatrixUser(),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onOpenUserProfile = callback,
|
||||
onOpenAccountSettings = callback,
|
||||
)
|
||||
onNodeWithText("Alice").performClick()
|
||||
}
|
||||
@@ -178,84 +175,85 @@ class PreferencesRootViewTest : RobolectricTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Encryption invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
fun `clicking on another theme emits the expected event`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>()
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
clickOn(CommonStrings.common_appearance)
|
||||
clickOn(R.string.theme_dark)
|
||||
eventsRecorder.assertSingle(PreferencesRootEvent.SetTheme(ThemeOption.Dark))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `black theme is shown when available`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
availableThemeOptions = ThemeOption.entries.toImmutableList(),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
clickOn(CommonStrings.common_appearance)
|
||||
onNodeWithText(activity!!.getString(R.string.theme_black)).assertExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `black theme is hidden when unavailable`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
availableThemeOptions = ThemeOption.entries.filterNot { it == ThemeOption.Black }.toImmutableList(),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
clickOn(CommonStrings.common_appearance)
|
||||
assertNoNodeWithText(R.string.theme_black)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Media upload quality invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
showSecureBackup = true,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onSecureBackupClick = callback,
|
||||
onOpenMediaSettings = callback,
|
||||
)
|
||||
clickOn(CommonStrings.common_encryption)
|
||||
clickOn(CommonStrings.common_media_upload_quality)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when showSecureBackup is false, item is not shown`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
showSecureBackup = false,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
onNodeWithText(activity!!.getString(CommonStrings.common_encryption)).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Manage account invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnceWithParam("aUrl") { callback ->
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
accountManagementUrl = "aUrl",
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onManageAccountClick = callback,
|
||||
)
|
||||
clickOn(CommonStrings.action_manage_account_and_devices)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when accountManagementUrl is null, item is not shown`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
accountManagementUrl = null,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
onNodeWithText(activity!!.getString(CommonStrings.action_manage_account_and_devices)).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Link new devices invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
fun `click on Screen lock invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
showLinkNewDevice = true,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onLinkNewDeviceClick = callback,
|
||||
onOpenLockScreenSettings = callback,
|
||||
)
|
||||
clickOn(CommonStrings.common_link_new_device)
|
||||
clickOn(CommonStrings.common_screen_lock)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when showLinkNewDevice is false, item is not shown`() = runAndroidComposeUiTest {
|
||||
fun `click on Location sharing invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
showLinkNewDevice = false,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
onNodeWithText(activity!!.getString(CommonStrings.common_link_new_device)).assertDoesNotExist()
|
||||
ensureCalledOnce { callback ->
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onOpenLocationSettings = callback,
|
||||
)
|
||||
val text = activity!!.getString(CommonStrings.common_location_sharing)
|
||||
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -287,45 +285,31 @@ class PreferencesRootViewTest : RobolectricTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Report a problem invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
fun `click on Labs invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
canReportBug = true,
|
||||
showLabsItem = true,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onOpenRageShake = callback,
|
||||
onOpenLabs = callback,
|
||||
)
|
||||
val text = activity!!.getString(CommonStrings.common_report_a_problem)
|
||||
val text = activity!!.getString(R.string.screen_labs_title)
|
||||
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when canReportBug is false, item is not shown`() = runAndroidComposeUiTest {
|
||||
fun `when showLabsItem is false, item is not shown`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
canReportBug = false,
|
||||
showLabsItem = false,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
onNodeWithText(activity!!.getString(CommonStrings.common_report_a_problem)).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Screen lock invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onOpenLockScreenSettings = callback,
|
||||
)
|
||||
clickOn(CommonStrings.common_screen_lock)
|
||||
}
|
||||
onNodeWithText(activity!!.getString(R.string.screen_labs_title)).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -338,7 +322,8 @@ class PreferencesRootViewTest : RobolectricTest() {
|
||||
),
|
||||
onOpenAbout = callback,
|
||||
)
|
||||
clickOn(CommonStrings.common_about)
|
||||
val text = activity!!.getString(CommonStrings.common_about)
|
||||
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,128 +356,15 @@ class PreferencesRootViewTest : RobolectricTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Advanced settings invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onOpenAdvancedSettings = callback,
|
||||
)
|
||||
clickOn(CommonStrings.common_advanced_settings)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Labs invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
showLabsItem = true,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onOpenLabs = callback,
|
||||
)
|
||||
clickOn(R.string.screen_labs_title)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when showLabsItem is false, item is not shown`() = runAndroidComposeUiTest {
|
||||
fun `when userStatusState is null, the status section is not shown`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
showLabsItem = false,
|
||||
userStatusState = null,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
onNodeWithText(activity!!.getString(R.string.screen_labs_title)).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Notification invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onOpenNotificationSettings = callback,
|
||||
)
|
||||
clickOn(R.string.screen_notification_settings_title)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Blocked users invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
nbOfBlockedUsers = 1,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onOpenBlockedUsers = callback,
|
||||
)
|
||||
clickOn(CommonStrings.common_blocked_users)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when nbOfBlockedUsers is 0, item is not shown`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
nbOfBlockedUsers = 0,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
onNodeWithText(activity!!.getString(CommonStrings.common_blocked_users)).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Remove this device invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onSignOutClick = callback,
|
||||
)
|
||||
val text = activity!!.getString(CommonStrings.action_signout)
|
||||
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Deactivate invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
canDeactivateAccount = true,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onDeactivateClick = callback,
|
||||
)
|
||||
val text = activity!!.getString(CommonStrings.action_delete_account)
|
||||
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when canDeactivateAccount is false, item is not shown`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
canDeactivateAccount = false,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
onNodeWithText(activity!!.getString(CommonStrings.action_delete_account)).assertDoesNotExist()
|
||||
assertNoNodeWithText(R.string.screen_settings_user_status_placeholder)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -514,21 +386,14 @@ private fun AndroidComposeUiTest<ComponentActivity>.setView(
|
||||
state: PreferencesRootState,
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
onAddAccountClick: () -> Unit = EnsureNeverCalled(),
|
||||
onSecureBackupClick: () -> Unit = EnsureNeverCalled(),
|
||||
onManageAccountClick: (url: String) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onLinkNewDeviceClick: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenAnalytics: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenRageShake: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenLockScreenSettings: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenAbout: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenDeveloperSettings: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenAdvancedSettings: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenMediaSettings: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenLocationSettings: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenLabs: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenNotificationSettings: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenUserProfile: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onOpenBlockedUsers: () -> Unit = EnsureNeverCalled(),
|
||||
onSignOutClick: () -> Unit = EnsureNeverCalled(),
|
||||
onDeactivateClick: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenAccountSettings: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
PreferencesRootView(
|
||||
@@ -536,21 +401,14 @@ private fun AndroidComposeUiTest<ComponentActivity>.setView(
|
||||
emojiPickerRenderer = NoOpEmojiPickerRenderer,
|
||||
onBackClick = onBackClick,
|
||||
onAddAccountClick = onAddAccountClick,
|
||||
onSecureBackupClick = onSecureBackupClick,
|
||||
onManageAccountClick = onManageAccountClick,
|
||||
onLinkNewDeviceClick = onLinkNewDeviceClick,
|
||||
onOpenAnalytics = onOpenAnalytics,
|
||||
onOpenRageShake = onOpenRageShake,
|
||||
onOpenLockScreenSettings = onOpenLockScreenSettings,
|
||||
onOpenAbout = onOpenAbout,
|
||||
onOpenDeveloperSettings = onOpenDeveloperSettings,
|
||||
onOpenMediaSettings = onOpenAdvancedSettings,
|
||||
onOpenMediaSettings = onOpenMediaSettings,
|
||||
onOpenLocationSettings = onOpenLocationSettings,
|
||||
onOpenLabs = onOpenLabs,
|
||||
onOpenNotificationSettings = onOpenNotificationSettings,
|
||||
onOpenUserProfile = onOpenUserProfile,
|
||||
onOpenBlockedUsers = onOpenBlockedUsers,
|
||||
onSignOutClick = onSignOutClick,
|
||||
onDeactivateClick = onDeactivateClick,
|
||||
onOpenAccountSettings = onOpenAccountSettings,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user