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