From 55223e65b161a7b55592f347bd1d6b6913bc53f9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 20:02:14 +0000 Subject: [PATCH 01/12] fix(deps): update dependency androidx.compose:compose-bom to v2025.04.01 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 33cc6b8095..0c9a34dac5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,7 @@ media3 = "1.7.1" camera = "1.4.2" # Compose -compose_bom = "2025.04.00" +compose_bom = "2025.04.01" composecompiler = "1.5.15" # Coroutines From d6e78d6f72ee78d8b58e5c1d91cc116d228282f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 24 Apr 2025 11:45:53 +0200 Subject: [PATCH 02/12] Fix autofill deprecations --- .../logout/impl/AccountDeactivationView.kt | 16 +++---- .../loginpassword/LoginPasswordView.kt | 27 ++++-------- .../impl/setup/views/RecoveryKeyView.kt | 12 +++--- .../designsystem/modifiers/Autofill.kt | 43 ------------------- 4 files changed, 21 insertions(+), 77 deletions(-) delete mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Autofill.kt diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt index b7d9ab4ba1..9e61f030c5 100644 --- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt +++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt @@ -34,10 +34,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.autofill.AutofillType +import androidx.compose.ui.autofill.ContentType import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentType +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation @@ -54,7 +56,6 @@ import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrgani import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.form.textFieldState import io.element.android.libraries.designsystem.components.list.SwitchListItem -import io.element.android.libraries.designsystem.modifiers.autofill import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusNext import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -276,14 +277,9 @@ private fun Content( .fillMaxWidth() .onTabOrEnterKeyFocusNext(focusManager) .testTag(TestTags.loginPassword) - .autofill( - autofillTypes = listOf(AutofillType.Password), - onFill = { - val sanitized = it.sanitize() - passwordFieldState = sanitized - eventSink(AccountDeactivationEvents.SetPassword(sanitized)) - } - ), + .semantics { + contentType = ContentType.Password + }, onValueChange = { val sanitized = it.sanitize() passwordFieldState = sanitized diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt index 4bca66dc96..2ea63c3955 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt @@ -30,10 +30,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.autofill.AutofillType +import androidx.compose.ui.autofill.ContentType import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentType +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation @@ -51,7 +53,6 @@ import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.form.textFieldState -import io.element.android.libraries.designsystem.modifiers.autofill import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusNext import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -175,14 +176,9 @@ private fun LoginForm( .fillMaxWidth() .onTabOrEnterKeyFocusNext(focusManager) .testTag(TestTags.loginEmailUsername) - .autofill( - autofillTypes = listOf(AutofillType.Username), - onFill = { - val sanitized = it.sanitize() - loginFieldState = sanitized - eventSink(LoginPasswordEvents.SetLogin(sanitized)) - } - ), + .semantics { + contentType = ContentType.Username + }, placeholder = stringResource(CommonStrings.common_username), onValueChange = { val sanitized = it.sanitize() @@ -227,14 +223,9 @@ private fun LoginForm( .fillMaxWidth() .onTabOrEnterKeyFocusNext(focusManager) .testTag(TestTags.loginPassword) - .autofill( - autofillTypes = listOf(AutofillType.Password), - onFill = { - val sanitized = it.sanitize() - passwordFieldState = sanitized - eventSink(LoginPasswordEvents.SetPassword(sanitized)) - } - ), + .semantics { + contentType = ContentType.Password + }, onValueChange = { val sanitized = it.sanitize() passwordFieldState = sanitized diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt index ecd9cd522f..9c24d463ba 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt @@ -24,10 +24,12 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.autofill.AutofillType +import androidx.compose.ui.autofill.ContentType import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentType +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -39,7 +41,6 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.securebackup.impl.R import io.element.android.features.securebackup.impl.tools.RecoveryKeyVisualTransformation -import io.element.android.libraries.designsystem.modifiers.autofill import io.element.android.libraries.designsystem.modifiers.clickableIfNotNull import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -186,10 +187,9 @@ private fun RecoveryKeyFormContent( modifier = Modifier .fillMaxWidth() .testTag(TestTags.recoveryKey) - .autofill( - autofillTypes = listOf(AutofillType.Password), - onFill = { onChange(it) }, - ), + .semantics { + contentType = ContentType.Password + }, minLines = 2, value = state.formattedRecoveryKey.orEmpty(), onValueChange = onChange, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Autofill.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Autofill.kt deleted file mode 100644 index 5436a0330c..0000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Autofill.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2024 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.libraries.designsystem.modifiers - -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.autofill.AutofillNode -import androidx.compose.ui.autofill.AutofillType -import androidx.compose.ui.composed -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.layout.boundsInWindow -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalAutofill -import androidx.compose.ui.platform.LocalAutofillTree - -@Suppress("ModifierComposed") -@OptIn(ExperimentalComposeUiApi::class) -fun Modifier.autofill(autofillTypes: List, onFill: (String) -> Unit) = composed { - val autofillNode = AutofillNode(autofillTypes, onFill = onFill) - LocalAutofillTree.current += autofillNode - - val autofill = LocalAutofill.current - - this - .onGloballyPositioned { - // Inform autofill framework of where our composable is so it can show the popup in the right place - autofillNode.boundingBox = it.boundsInWindow() - } - .onFocusChanged { - autofill?.run { - if (it.isFocused) { - requestAutofillForNode(autofillNode) - } else { - cancelAutofillForNode(autofillNode) - } - } - } -} From 163ae1469b6f3470571999d46c87a1c0cdf01377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 24 Apr 2025 11:46:10 +0200 Subject: [PATCH 03/12] Adapt our custom BottomSheetState and scaffold to the new APIs --- .../bottomsheet/CustomBottomSheetScaffold.kt | 46 ++++++------ .../bottomsheet/CustomSheetState.kt | 72 +++---------------- 2 files changed, 29 insertions(+), 89 deletions(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomBottomSheetScaffold.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomBottomSheetScaffold.kt index a819d9770b..5b7aaf52a9 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomBottomSheetScaffold.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomBottomSheetScaffold.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.designsystem.theme.components.bottomsheet +import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.DraggableAnchors import androidx.compose.foundation.gestures.Orientation @@ -230,7 +231,6 @@ private fun CustomStandardBottomSheet( ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection( sheetState = state, orientation = orientation, - onFling = { scope.launch { state.settle(it) } } ) } ) @@ -246,7 +246,7 @@ private fun CustomStandardBottomSheet( val newTarget = when (state.anchoredDraggableState.targetValue) { SheetValue.Hidden, SheetValue.PartiallyExpanded -> SheetValue.PartiallyExpanded SheetValue.Expanded -> { - if (newAnchors.hasAnchorFor(SheetValue.Expanded)) SheetValue.Expanded else SheetValue.PartiallyExpanded + if (newAnchors.hasPositionFor(SheetValue.Expanded)) SheetValue.Expanded else SheetValue.PartiallyExpanded } } state.anchoredDraggableState.updateAnchors(newAnchors, newTarget) @@ -336,8 +336,8 @@ internal fun DraggableAnchors( ): DraggableAnchors = MapDraggableAnchors(DraggableAnchorsConfig().apply(builder).anchors) private class MapDraggableAnchors(private val anchors: Map) : DraggableAnchors { - override fun positionOf(value: T): Float = anchors[value] ?: Float.NaN - override fun hasAnchorFor(value: T) = anchors.containsKey(value) + override fun positionOf(anchor: T): Float = anchors[anchor] ?: Float.NaN + override fun hasPositionFor(anchor: T) = anchors.containsKey(anchor) override fun closestAnchor(position: Float): T? = anchors.minByOrNull { abs(position - it.value) @@ -353,13 +353,20 @@ private class MapDraggableAnchors(private val anchors: Map) : Dragg }?.key } - override fun minAnchor() = anchors.values.minOrNull() ?: Float.NaN + override fun minPosition() = anchors.values.minOrNull() ?: Float.NaN + override fun positionAt(index: Int): Float { + return anchorAt(index)?.let { anchors[it] } ?: Float.NaN + } - override fun maxAnchor() = anchors.values.maxOrNull() ?: Float.NaN + override fun maxPosition() = anchors.values.maxOrNull() ?: Float.NaN override val size: Int get() = anchors.size + override fun anchorAt(index: Int): T? { + return anchors.keys.elementAtOrNull(index) + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is MapDraggableAnchors<*>) return false @@ -367,23 +374,16 @@ private class MapDraggableAnchors(private val anchors: Map) : Dragg return anchors == other.anchors } - override fun forEach(block: (anchor: T, position: Float) -> Unit) { - for (anchor in anchors) { - block(anchor.key, anchor.value) - } - } - override fun hashCode() = 31 * anchors.hashCode() override fun toString() = "MapDraggableAnchors($anchors)" } -@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) -@SuppressWarnings("FunctionName") +@OptIn(ExperimentalMaterial3Api::class) +@SuppressWarnings("FunctionName", "standard:function-naming") internal fun ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection( sheetState: CustomSheetState, orientation: Orientation, - onFling: (velocity: Float) -> Unit ): NestedScrollConnection = object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { val delta = available.toFloat() @@ -409,9 +409,10 @@ internal fun ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection( override suspend fun onPreFling(available: Velocity): Velocity { val toFling = available.toFloat() val currentOffset = sheetState.requireOffset() - val minAnchor = sheetState.anchoredDraggableState.anchors.minAnchor() + val minAnchor = sheetState.anchoredDraggableState.anchors.minPosition() + val animationSpec = tween() return if (toFling < 0 && currentOffset > minAnchor) { - onFling(toFling) + sheetState.settle(animationSpec) // since we go to the anchor with tween settling, consume all for the best UX available } else { @@ -420,7 +421,8 @@ internal fun ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection( } override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - onFling(available.toFloat()) + val animationSpec = tween() + sheetState.settle(animationSpec) return available } @@ -476,32 +478,27 @@ fun rememberBottomSheetScaffoldState( * * @param initialValue the initial value of the state. Should be either [PartiallyExpanded] or * [Expanded] if [skipHiddenState] is true - * @param confirmValueChange optional callback invoked to confirm or veto a pending state change * @param [skipHiddenState] whether Hidden state is skipped for [BottomSheetScaffold] */ @Composable @ExperimentalMaterial3Api fun rememberStandardBottomSheetState( initialValue: SheetValue = PartiallyExpanded, - confirmValueChange: (SheetValue) -> Boolean = { true }, skipHiddenState: Boolean = true, -) = rememberSheetState(false, confirmValueChange, initialValue, skipHiddenState) +) = rememberSheetState(false, initialValue, skipHiddenState) @Composable @ExperimentalMaterial3Api internal fun rememberSheetState( skipPartiallyExpanded: Boolean = false, - confirmValueChange: (SheetValue) -> Boolean = { true }, initialValue: SheetValue = SheetValue.Hidden, skipHiddenState: Boolean = false, ): CustomSheetState { val density = LocalDensity.current return rememberSaveable( skipPartiallyExpanded, - confirmValueChange, saver = CustomSheetState.Saver( skipPartiallyExpanded = skipPartiallyExpanded, - confirmValueChange = confirmValueChange, density = density ) ) { @@ -509,7 +506,6 @@ internal fun rememberSheetState( skipPartiallyExpanded, density, initialValue, - confirmValueChange, skipHiddenState ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomSheetState.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomSheetState.kt index 85507485e3..c696d76015 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomSheetState.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomSheetState.kt @@ -7,9 +7,7 @@ package io.element.android.libraries.designsystem.theme.components.bottomsheet -import androidx.compose.animation.core.SpringSpec -import androidx.compose.animation.core.exponentialDecay -import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.animation.core.AnimationSpec import androidx.compose.foundation.gestures.animateTo import androidx.compose.foundation.gestures.snapTo import androidx.compose.material3.ExperimentalMaterial3Api @@ -21,10 +19,8 @@ import androidx.compose.material3.SheetValue.PartiallyExpanded import androidx.compose.runtime.Stable import androidx.compose.runtime.saveable.Saver import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.dp import kotlinx.coroutines.CancellationException -@OptIn(ExperimentalFoundationApi::class) @Stable @ExperimentalMaterial3Api class CustomSheetState @@ -34,13 +30,12 @@ class CustomSheetState replaceWith = ReplaceWith( "SheetState(" + "skipPartiallyExpanded, LocalDensity.current, initialValue, " + - "confirmValueChange, skipHiddenState)" + "skipHiddenState)" ) ) constructor( internal val skipPartiallyExpanded: Boolean, initialValue: SheetValue = Hidden, - confirmValueChange: (SheetValue) -> Boolean = { true }, internal val skipHiddenState: Boolean = false, ) { /** @@ -54,7 +49,6 @@ constructor( * interaction. * @param density The density that this state can use to convert values to and from dp. * @param initialValue The initial value of the state. - * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change. * @param skipHiddenState Whether the hidden state should be skipped. If true, the sheet will always * expand to the [Expanded] state and move to the [PartiallyExpanded] if available, either * programmatically or by user interaction. @@ -65,9 +59,8 @@ constructor( skipPartiallyExpanded: Boolean, density: Density, initialValue: SheetValue = Hidden, - confirmValueChange: (SheetValue) -> Boolean = { true }, skipHiddenState: Boolean = false, - ) : this(skipPartiallyExpanded, initialValue, confirmValueChange, skipHiddenState) { + ) : this(skipPartiallyExpanded, initialValue, skipHiddenState) { this.density = density } @@ -135,13 +128,13 @@ constructor( */ val hasExpandedState: Boolean - get() = anchoredDraggableState.anchors.hasAnchorFor(Expanded) + get() = anchoredDraggableState.anchors.hasPositionFor(Expanded) /** * Whether the modal bottom sheet has a partially expanded state defined. */ val hasPartiallyExpandedState: Boolean - get() = anchoredDraggableState.anchors.hasAnchorFor(PartiallyExpanded) + get() = anchoredDraggableState.anchors.hasPositionFor(PartiallyExpanded) /** * Fully expand the bottom sheet with animation and suspend until it is fully expanded or @@ -203,7 +196,6 @@ constructor( * * @param targetValue The target value of the animation */ - @OptIn(ExperimentalFoundationApi::class) internal suspend fun animateTo( targetValue: SheetValue, ) { @@ -218,7 +210,6 @@ constructor( * * @param targetValue The target value of the animation */ - @OptIn(ExperimentalFoundationApi::class) internal suspend fun snapTo(targetValue: SheetValue) { anchoredDraggableState.snapTo(targetValue) } @@ -226,29 +217,17 @@ constructor( /** * Find the closest anchor taking into account the velocity and settle at it with an animation. */ - @OptIn(ExperimentalFoundationApi::class) - internal suspend fun settle(velocity: Float) { - anchoredDraggableState.settle(velocity) + internal suspend fun settle(animationSpec: AnimationSpec) { + anchoredDraggableState.settle(animationSpec) } - @OptIn(ExperimentalFoundationApi::class) internal var anchoredDraggableState = androidx.compose.foundation.gestures.AnchoredDraggableState( initialValue = initialValue, - snapAnimationSpec = AnchoredDraggableDefaults.SnapAnimationSpec, - decayAnimationSpec = AnchoredDraggableDefaults.DecayAnimationSpec, - confirmValueChange = confirmValueChange, - positionalThreshold = { with(requireDensity()) { 56.dp.toPx() } }, - velocityThreshold = { with(requireDensity()) { 125.dp.toPx() } } ) - @OptIn(ExperimentalFoundationApi::class) internal val offset: Float? get() = anchoredDraggableState.offset internal var density: Density? = null - private fun requireDensity() = requireNotNull(density) { - "SheetState did not have a density attached. Are you using SheetState with " + - "BottomSheetScaffold or ModalBottomSheet component?" - } companion object { /** @@ -257,47 +236,12 @@ constructor( @SuppressWarnings("FunctionName") fun Saver( skipPartiallyExpanded: Boolean, - confirmValueChange: (SheetValue) -> Boolean, density: Density ) = Saver( save = { it.currentValue }, restore = { savedValue -> - CustomSheetState(skipPartiallyExpanded, density, savedValue, confirmValueChange) - } - ) - - /** - * The default [Saver] implementation for [SheetState]. - */ - @Deprecated( - message = "This function is deprecated. Please use the overload where Density is" + - " provided.", - replaceWith = ReplaceWith( - "Saver(skipPartiallyExpanded, confirmValueChange, LocalDensity.current)" - ) - ) - @Suppress("Deprecation", "FunctionName") - fun Saver( - skipPartiallyExpanded: Boolean, - confirmValueChange: (SheetValue) -> Boolean - ) = Saver( - save = { it.currentValue }, - restore = { savedValue -> - CustomSheetState(skipPartiallyExpanded, savedValue, confirmValueChange) + CustomSheetState(skipPartiallyExpanded, density, savedValue) } ) } } - -@Stable -@ExperimentalMaterial3Api -internal object AnchoredDraggableDefaults { - /** - * The default animation used by [AnchoredDraggableState]. - */ - @ExperimentalMaterial3Api - val SnapAnimationSpec = SpringSpec() - - @ExperimentalMaterial3Api - val DecayAnimationSpec = exponentialDecay() -} From f8283e3f3c09485cb4bb1b6416326b7d5f17be66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 24 Apr 2025 11:56:29 +0200 Subject: [PATCH 04/12] Get rid of all the custom bottom sheet implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It doesn't seem to be needed anymore 🎉 --- .../location/impl/send/SendLocationView.kt | 4 +- .../impl/ExpandableBottomSheetScaffold.kt | 16 +- .../theme/components/BottomSheetScaffold.kt | 7 +- .../bottomsheet/CustomBottomSheetScaffold.kt | 512 ------------------ .../bottomsheet/CustomSheetState.kt | 247 --------- 5 files changed, 10 insertions(+), 776 deletions(-) delete mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomBottomSheetScaffold.kt delete mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomSheetState.kt diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt index 8f5ef7897c..53d1855e2a 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt @@ -20,6 +20,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ListItem import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetScaffoldState +import androidx.compose.material3.rememberStandardBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment @@ -46,8 +48,6 @@ import io.element.android.libraries.designsystem.theme.components.FloatingAction import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar -import io.element.android.libraries.designsystem.theme.components.bottomsheet.rememberBottomSheetScaffoldState -import io.element.android.libraries.designsystem.theme.components.bottomsheet.rememberStandardBottomSheetState import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.maplibre.compose.CameraMode import io.element.android.libraries.maplibre.compose.CameraMoveStartedReason diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt index 21b8d03381..ca70ca55de 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt @@ -13,6 +13,8 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetScaffoldState +import androidx.compose.material3.rememberStandardBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -29,10 +31,8 @@ import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.min +import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.designsystem.theme.components.BottomSheetScaffold -import io.element.android.libraries.designsystem.theme.components.bottomsheet.CustomSheetState -import io.element.android.libraries.designsystem.theme.components.bottomsheet.rememberBottomSheetScaffoldState -import io.element.android.libraries.designsystem.theme.components.bottomsheet.rememberStandardBottomSheetState import kotlin.math.roundToInt /** @@ -139,8 +139,8 @@ internal fun ExpandableBottomSheetScaffold( modifier = Modifier.fillMaxHeight(), measurePolicy = { measurables, constraints -> val constraintHeight = constraints.maxHeight - val offset = scaffoldState.bottomSheetState.getIntOffset() ?: 0 - val height = Integer.max(0, constraintHeight - offset) + val offset = tryOrNull { scaffoldState.bottomSheetState.requireOffset() } ?: 0f + val height = Integer.max(0, constraintHeight - offset.roundToInt()) val top = measurables[0].measure( constraints.copy( minHeight = height, @@ -165,12 +165,6 @@ internal fun ExpandableBottomSheetScaffold( ) } -private fun CustomSheetState.getIntOffset(): Int? = try { - requireOffset().roundToInt() -} catch (e: IllegalStateException) { - null -} - private sealed interface Slot { data class SheetContent(val key: Int?) : Slot data object DragHandle : Slot diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetScaffold.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetScaffold.kt index 5ac1dedb4a..d4db8923a0 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetScaffold.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetScaffold.kt @@ -10,19 +10,18 @@ package io.element.android.libraries.designsystem.theme.components import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.BottomSheetScaffoldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.contentColorFor +import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Dp -import io.element.android.libraries.designsystem.theme.components.bottomsheet.BottomSheetScaffoldState -import io.element.android.libraries.designsystem.theme.components.bottomsheet.CustomBottomSheetScaffold -import io.element.android.libraries.designsystem.theme.components.bottomsheet.rememberBottomSheetScaffoldState @Composable @ExperimentalMaterial3Api @@ -44,7 +43,7 @@ fun BottomSheetScaffold( contentColor: Color = contentColorFor(containerColor), content: @Composable (PaddingValues) -> Unit ) { - CustomBottomSheetScaffold( + androidx.compose.material3.BottomSheetScaffold( sheetContent = sheetContent, modifier = modifier, scaffoldState = scaffoldState, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomBottomSheetScaffold.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomBottomSheetScaffold.kt deleted file mode 100644 index 5b7aaf52a9..0000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomBottomSheetScaffold.kt +++ /dev/null @@ -1,512 +0,0 @@ -/* - * Copyright 2023, 2024 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(ExperimentalFoundationApi::class) - -package io.element.android.libraries.designsystem.theme.components.bottomsheet - -import androidx.compose.animation.core.tween -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.gestures.DraggableAnchors -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.anchoredDraggable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.requiredHeightIn -import androidx.compose.foundation.layout.widthIn -import androidx.compose.material3.BottomSheetDefaults -import androidx.compose.material3.BottomSheetScaffold -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SheetState -import androidx.compose.material3.SheetValue -import androidx.compose.material3.SheetValue.Expanded -import androidx.compose.material3.SheetValue.PartiallyExpanded -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Surface -import androidx.compose.material3.contentColorFor -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.layout.SubcomposeLayout -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.semantics.collapse -import androidx.compose.ui.semantics.dismiss -import androidx.compose.ui.semantics.expand -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.Velocity -import kotlinx.coroutines.launch -import kotlin.math.abs -import kotlin.math.roundToInt - -// These are needed until https://issuetracker.google.com/issues/306464779 is fixed - -@Composable -@ExperimentalMaterial3Api -fun CustomBottomSheetScaffold( - sheetContent: @Composable ColumnScope.() -> Unit, - modifier: Modifier = Modifier, - scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(), - sheetPeekHeight: Dp = BottomSheetDefaults.SheetPeekHeight, - sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth, - sheetShape: Shape = BottomSheetDefaults.ExpandedShape, - sheetContainerColor: Color = Color.White, - sheetContentColor: Color = contentColorFor(sheetContainerColor), - sheetTonalElevation: Dp = BottomSheetDefaults.Elevation, - sheetShadowElevation: Dp = BottomSheetDefaults.Elevation, - sheetDragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() }, - sheetSwipeEnabled: Boolean = true, - topBar: @Composable (() -> Unit)? = null, - snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, - containerColor: Color = MaterialTheme.colorScheme.surface, - contentColor: Color = contentColorFor(containerColor), - content: @Composable (PaddingValues) -> Unit -) { - val peekHeightPx = with(LocalDensity.current) { - sheetPeekHeight.roundToPx() - } - CustomBottomSheetScaffoldLayout( - modifier = modifier, - topBar = topBar, - body = content, - snackbarHost = { - snackbarHost(scaffoldState.snackbarHostState) - }, - sheetPeekHeight = sheetPeekHeight, - sheetOffset = { scaffoldState.bottomSheetState.requireOffset() }, - sheetState = scaffoldState.bottomSheetState, - containerColor = containerColor, - contentColor = contentColor, - bottomSheet = { layoutHeight -> - CustomStandardBottomSheet( - state = scaffoldState.bottomSheetState, - peekHeight = sheetPeekHeight, - sheetMaxWidth = sheetMaxWidth, - sheetSwipeEnabled = sheetSwipeEnabled, - calculateAnchors = { sheetSize -> - val sheetHeight = sheetSize.height - io.element.android.libraries.designsystem.theme.components.bottomsheet.DraggableAnchors { - if (!scaffoldState.bottomSheetState.skipPartiallyExpanded) { - PartiallyExpanded at (layoutHeight - peekHeightPx).toFloat() - } - if (sheetHeight != peekHeightPx) { - Expanded at maxOf(layoutHeight - sheetHeight, 0).toFloat() - } - if (!scaffoldState.bottomSheetState.skipHiddenState) { - SheetValue.Hidden at layoutHeight.toFloat() - } - } - }, - shape = sheetShape, - containerColor = sheetContainerColor, - contentColor = sheetContentColor, - tonalElevation = sheetTonalElevation, - shadowElevation = sheetShadowElevation, - dragHandle = sheetDragHandle, - content = sheetContent - ) - } - ) -} - -@SuppressWarnings("ModifierWithoutDefault") -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun CustomBottomSheetScaffoldLayout( - modifier: Modifier, - topBar: @Composable (() -> Unit)?, - body: @Composable (innerPadding: PaddingValues) -> Unit, - bottomSheet: @Composable (layoutHeight: Int) -> Unit, - snackbarHost: @Composable () -> Unit, - sheetPeekHeight: Dp, - sheetOffset: () -> Float, - sheetState: CustomSheetState, - containerColor: Color, - contentColor: Color, -) { - // b/291735717 Remove this once deprecated methods without density are removed - val density = LocalDensity.current - SideEffect { - sheetState.density = density - } - SubcomposeLayout { constraints -> - val layoutWidth = constraints.maxWidth - val layoutHeight = constraints.maxHeight - val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) - - val sheetPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Sheet) { - bottomSheet(layoutHeight) - }[0].measure(looseConstraints) - - val topBarPlaceable = topBar?.let { - subcompose(BottomSheetScaffoldLayoutSlot.TopBar) { topBar() }[0] - .measure(looseConstraints) - } - val topBarHeight = topBarPlaceable?.height ?: 0 - - val bodyConstraints = looseConstraints.copy(maxHeight = layoutHeight - topBarHeight) - val bodyPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Body) { - Surface( - modifier = modifier, - color = containerColor, - contentColor = contentColor, - ) { body(PaddingValues(bottom = sheetPeekHeight)) } - }[0].measure(bodyConstraints) - - val snackbarPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Snackbar, snackbarHost)[0] - .measure(looseConstraints) - - layout(layoutWidth, layoutHeight) { - val sheetOffsetY = sheetOffset().roundToInt() - val sheetOffsetX = Integer.max(0, (layoutWidth - sheetPlaceable.width) / 2) - - val snackbarOffsetX = (layoutWidth - snackbarPlaceable.width) / 2 - val snackbarOffsetY = when (sheetState.currentValue) { - SheetValue.PartiallyExpanded -> sheetOffsetY - snackbarPlaceable.height - SheetValue.Expanded, SheetValue.Hidden -> layoutHeight - snackbarPlaceable.height - } - - // Placement order is important for elevation - bodyPlaceable.placeRelative(0, topBarHeight) - topBarPlaceable?.placeRelative(0, 0) - sheetPlaceable.placeRelative(sheetOffsetX, sheetOffsetY) - snackbarPlaceable.placeRelative(snackbarOffsetX, snackbarOffsetY) - } - } -} - -private enum class BottomSheetScaffoldLayoutSlot { TopBar, Body, Sheet, Snackbar } - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) -@Composable -private fun CustomStandardBottomSheet( - state: CustomSheetState, - @Suppress("PrimitiveInLambda") - calculateAnchors: (sheetSize: IntSize) -> DraggableAnchors, - peekHeight: Dp, - sheetMaxWidth: Dp, - sheetSwipeEnabled: Boolean, - shape: Shape, - containerColor: Color, - contentColor: Color, - tonalElevation: Dp, - shadowElevation: Dp, - dragHandle: @Composable (() -> Unit)?, - content: @Composable ColumnScope.() -> Unit -) { - val scope = rememberCoroutineScope() - - val orientation = Orientation.Vertical - - Surface( - modifier = Modifier - .widthIn(max = sheetMaxWidth) - .fillMaxWidth() - .requiredHeightIn(min = peekHeight) - .apply { - if (sheetSwipeEnabled) { - nestedScroll( - remember(state.anchoredDraggableState) { - ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection( - sheetState = state, - orientation = orientation, - ) - } - ) - } - } - .anchoredDraggable( - state = state.anchoredDraggableState, - orientation = orientation, - enabled = sheetSwipeEnabled - ) - .onSizeChanged { layoutSize -> - val newAnchors = calculateAnchors(layoutSize) - val newTarget = when (state.anchoredDraggableState.targetValue) { - SheetValue.Hidden, SheetValue.PartiallyExpanded -> SheetValue.PartiallyExpanded - SheetValue.Expanded -> { - if (newAnchors.hasPositionFor(SheetValue.Expanded)) SheetValue.Expanded else SheetValue.PartiallyExpanded - } - } - state.anchoredDraggableState.updateAnchors(newAnchors, newTarget) - }, - shape = shape, - color = containerColor, - contentColor = contentColor, - tonalElevation = tonalElevation, - shadowElevation = shadowElevation, - ) { - Column(Modifier.fillMaxWidth()) { - if (dragHandle != null) { - val partialExpandActionLabel = - "Partial Expand" - val dismissActionLabel = "Dismiss" - val expandActionLabel = "Expand" - Box( - Modifier - .align(Alignment.CenterHorizontally) - .semantics(mergeDescendants = true) { - with(state) { - // Provides semantics to interact with the bottomsheet if there is more - // than one anchor to swipe to and swiping is enabled. - if (anchoredDraggableState.anchors.size > 1 && sheetSwipeEnabled) { - if (currentValue == SheetValue.PartiallyExpanded) { - expand(expandActionLabel) { - scope.launch { expand() } - true - } - } else { - collapse(partialExpandActionLabel) { - scope.launch { partialExpand() } - true - } - } - if (!state.skipHiddenState) { - dismiss(dismissActionLabel) { - scope.launch { hide() } - true - } - } - } - } - }, - ) { - dragHandle() - } - } - content() - } - } -} - -/** - * [DraggableAnchorsConfig] stores a mutable configuration anchors, comprised of values of [T] and - * corresponding [Float] positions. This [DraggableAnchorsConfig] is used to construct an immutable - * [DraggableAnchors] instance later on. - */ -@ExperimentalFoundationApi -class DraggableAnchorsConfig { - internal val anchors = mutableMapOf() - - /** - * Set the anchor position for [this] anchor. - * - * @param position The anchor position. - */ - @Suppress("BuilderSetStyle") - infix fun T.at(position: Float) { - anchors[this] = position - } -} - -/** - * Create a new [DraggableAnchors] instance using a builder function. - * - * @param T The type of the anchor values. - * @param builder A function with a [DraggableAnchorsConfig] that offers APIs to configure anchors - * @return A new [DraggableAnchors] instance with the anchor positions set by the `builder` - * function. - */ -@OptIn(ExperimentalFoundationApi::class) -@ExperimentalMaterial3Api -@SuppressWarnings("FunctionName") -internal fun DraggableAnchors( - builder: DraggableAnchorsConfig.() -> Unit -): DraggableAnchors = MapDraggableAnchors(DraggableAnchorsConfig().apply(builder).anchors) - -private class MapDraggableAnchors(private val anchors: Map) : DraggableAnchors { - override fun positionOf(anchor: T): Float = anchors[anchor] ?: Float.NaN - override fun hasPositionFor(anchor: T) = anchors.containsKey(anchor) - - override fun closestAnchor(position: Float): T? = anchors.minByOrNull { - abs(position - it.value) - }?.key - - override fun closestAnchor( - position: Float, - searchUpwards: Boolean - ): T? { - return anchors.minByOrNull { (_, anchor) -> - val delta = if (searchUpwards) anchor - position else position - anchor - if (delta < 0) Float.POSITIVE_INFINITY else delta - }?.key - } - - override fun minPosition() = anchors.values.minOrNull() ?: Float.NaN - override fun positionAt(index: Int): Float { - return anchorAt(index)?.let { anchors[it] } ?: Float.NaN - } - - override fun maxPosition() = anchors.values.maxOrNull() ?: Float.NaN - - override val size: Int - get() = anchors.size - - override fun anchorAt(index: Int): T? { - return anchors.keys.elementAtOrNull(index) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is MapDraggableAnchors<*>) return false - - return anchors == other.anchors - } - - override fun hashCode() = 31 * anchors.hashCode() - - override fun toString() = "MapDraggableAnchors($anchors)" -} - -@OptIn(ExperimentalMaterial3Api::class) -@SuppressWarnings("FunctionName", "standard:function-naming") -internal fun ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection( - sheetState: CustomSheetState, - orientation: Orientation, -): NestedScrollConnection = object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - val delta = available.toFloat() - return if (delta < 0 && source == NestedScrollSource.UserInput) { - sheetState.anchoredDraggableState.dispatchRawDelta(delta).toOffset() - } else { - Offset.Zero - } - } - - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource - ): Offset { - return if (source == NestedScrollSource.UserInput) { - sheetState.anchoredDraggableState.dispatchRawDelta(available.toFloat()).toOffset() - } else { - Offset.Zero - } - } - - override suspend fun onPreFling(available: Velocity): Velocity { - val toFling = available.toFloat() - val currentOffset = sheetState.requireOffset() - val minAnchor = sheetState.anchoredDraggableState.anchors.minPosition() - val animationSpec = tween() - return if (toFling < 0 && currentOffset > minAnchor) { - sheetState.settle(animationSpec) - // since we go to the anchor with tween settling, consume all for the best UX - available - } else { - Velocity.Zero - } - } - - override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - val animationSpec = tween() - sheetState.settle(animationSpec) - return available - } - - private fun Float.toOffset(): Offset = Offset( - x = if (orientation == Orientation.Horizontal) this else 0f, - y = if (orientation == Orientation.Vertical) this else 0f - ) - - @JvmName("velocityToFloat") - private fun Velocity.toFloat() = if (orientation == Orientation.Horizontal) x else y - - @JvmName("offsetToFloat") - private fun Offset.toFloat(): Float = if (orientation == Orientation.Horizontal) x else y -} - -/** - * State of the [BottomSheetScaffold] composable. - * - * @param bottomSheetState the state of the persistent bottom sheet - * @param snackbarHostState the [SnackbarHostState] used to show snackbars inside the scaffold - */ -@ExperimentalMaterial3Api -@Stable -@SuppressWarnings("UseDataClass") -class BottomSheetScaffoldState( - val bottomSheetState: CustomSheetState, - val snackbarHostState: SnackbarHostState -) - -/** - * Create and [remember] a [BottomSheetScaffoldState]. - * - * @param bottomSheetState the state of the standard bottom sheet. See - * [rememberStandardBottomSheetState] - * @param snackbarHostState the [SnackbarHostState] used to show snackbars inside the scaffold - */ -@Composable -@ExperimentalMaterial3Api -fun rememberBottomSheetScaffoldState( - bottomSheetState: CustomSheetState = rememberStandardBottomSheetState(), - snackbarHostState: SnackbarHostState = remember { SnackbarHostState() } -): BottomSheetScaffoldState { - return remember(bottomSheetState, snackbarHostState) { - BottomSheetScaffoldState( - bottomSheetState = bottomSheetState, - snackbarHostState = snackbarHostState - ) - } -} - -/** - * Create and [remember] a [SheetState] for [BottomSheetScaffold]. - * - * @param initialValue the initial value of the state. Should be either [PartiallyExpanded] or - * [Expanded] if [skipHiddenState] is true - * @param [skipHiddenState] whether Hidden state is skipped for [BottomSheetScaffold] - */ -@Composable -@ExperimentalMaterial3Api -fun rememberStandardBottomSheetState( - initialValue: SheetValue = PartiallyExpanded, - skipHiddenState: Boolean = true, -) = rememberSheetState(false, initialValue, skipHiddenState) - -@Composable -@ExperimentalMaterial3Api -internal fun rememberSheetState( - skipPartiallyExpanded: Boolean = false, - initialValue: SheetValue = SheetValue.Hidden, - skipHiddenState: Boolean = false, -): CustomSheetState { - val density = LocalDensity.current - return rememberSaveable( - skipPartiallyExpanded, - saver = CustomSheetState.Saver( - skipPartiallyExpanded = skipPartiallyExpanded, - density = density - ) - ) { - CustomSheetState( - skipPartiallyExpanded, - density, - initialValue, - skipHiddenState - ) - } -} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomSheetState.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomSheetState.kt deleted file mode 100644 index c696d76015..0000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomSheetState.kt +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright 2023, 2024 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.libraries.designsystem.theme.components.bottomsheet - -import androidx.compose.animation.core.AnimationSpec -import androidx.compose.foundation.gestures.animateTo -import androidx.compose.foundation.gestures.snapTo -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.SheetValue -import androidx.compose.material3.SheetValue.Expanded -import androidx.compose.material3.SheetValue.Hidden -import androidx.compose.material3.SheetValue.PartiallyExpanded -import androidx.compose.runtime.Stable -import androidx.compose.runtime.saveable.Saver -import androidx.compose.ui.unit.Density -import kotlinx.coroutines.CancellationException - -@Stable -@ExperimentalMaterial3Api -class CustomSheetState -@Deprecated( - message = "This constructor is deprecated. " + - "Please use the constructor that provides a [Density]", - replaceWith = ReplaceWith( - "SheetState(" + - "skipPartiallyExpanded, LocalDensity.current, initialValue, " + - "skipHiddenState)" - ) -) -constructor( - internal val skipPartiallyExpanded: Boolean, - initialValue: SheetValue = Hidden, - internal val skipHiddenState: Boolean = false, -) { - /** - * State of a sheet composable, such as [ModalBottomSheet] - * - * Contains states relating to its swipe position as well as animations between state values. - * - * @param skipPartiallyExpanded Whether the partially expanded state, if the sheet is large - * enough, should be skipped. If true, the sheet will always expand to the [Expanded] state and move - * to the [Hidden] state if available when hiding the sheet, either programmatically or by user - * interaction. - * @param density The density that this state can use to convert values to and from dp. - * @param initialValue The initial value of the state. - * @param skipHiddenState Whether the hidden state should be skipped. If true, the sheet will always - * expand to the [Expanded] state and move to the [PartiallyExpanded] if available, either - * programmatically or by user interaction. - */ - @ExperimentalMaterial3Api - @Suppress("Deprecation") - constructor( - skipPartiallyExpanded: Boolean, - density: Density, - initialValue: SheetValue = Hidden, - skipHiddenState: Boolean = false, - ) : this(skipPartiallyExpanded, initialValue, skipHiddenState) { - this.density = density - } - - init { - if (skipPartiallyExpanded) { - require(initialValue != PartiallyExpanded) { - "The initial value must not be set to PartiallyExpanded if skipPartiallyExpanded " + - "is set to true." - } - } - if (skipHiddenState) { - require(initialValue != Hidden) { - "The initial value must not be set to Hidden if skipHiddenState is set to true." - } - } - } - - /** - * The current value of the state. - * - * If no swipe or animation is in progress, this corresponds to the state the bottom sheet is - * currently in. If a swipe or an animation is in progress, this corresponds the state the sheet - * was in before the swipe or animation started. - */ - - val currentValue: SheetValue get() = anchoredDraggableState.currentValue - - /** - * The target value of the bottom sheet state. - * - * If a swipe is in progress, this is the value that the sheet would animate to if the - * swipe finishes. If an animation is running, this is the target value of that animation. - * Finally, if no swipe or animation is in progress, this is the same as the [currentValue]. - */ - val targetValue: SheetValue get() = anchoredDraggableState.targetValue - - /** - * Whether the modal bottom sheet is visible. - */ - val isVisible: Boolean - get() = anchoredDraggableState.currentValue != Hidden - - /** - * Require the current offset (in pixels) of the bottom sheet. - * - * The offset will be initialized during the first measurement phase of the provided sheet - * content. - * - * These are the phases: - * Composition { -> Effects } -> Layout { Measurement -> Placement } -> Drawing - * - * During the first composition, an [IllegalStateException] is thrown. In subsequent - * compositions, the offset will be derived from the anchors of the previous pass. Always prefer - * accessing the offset from a LaunchedEffect as it will be scheduled to be executed the next - * frame, after layout. - * - * @throws IllegalStateException If the offset has not been initialized yet - */ - fun requireOffset(): Float = anchoredDraggableState.requireOffset() - - fun getOffset(): Float? = anchoredDraggableState.offset.takeIf { !it.isNaN() } - - /** - * Whether the sheet has an expanded state defined. - */ - - val hasExpandedState: Boolean - get() = anchoredDraggableState.anchors.hasPositionFor(Expanded) - - /** - * Whether the modal bottom sheet has a partially expanded state defined. - */ - val hasPartiallyExpandedState: Boolean - get() = anchoredDraggableState.anchors.hasPositionFor(PartiallyExpanded) - - /** - * Fully expand the bottom sheet with animation and suspend until it is fully expanded or - * animation has been cancelled. - * * - * @throws [CancellationException] if the animation is interrupted - */ - suspend fun expand() { - anchoredDraggableState.animateTo(Expanded) - } - - /** - * Animate the bottom sheet and suspend until it is partially expanded or animation has been - * cancelled. - * @throws [CancellationException] if the animation is interrupted - * @throws [IllegalStateException] if [skipPartiallyExpanded] is set to true - */ - suspend fun partialExpand() { - check(!skipPartiallyExpanded) { - "Attempted to animate to partial expanded when skipPartiallyExpanded was enabled. Set" + - " skipPartiallyExpanded to false to use this function." - } - animateTo(PartiallyExpanded) - } - - /** - * Expand the bottom sheet with animation and suspend until it is [PartiallyExpanded] if defined - * else [Expanded]. - * @throws [CancellationException] if the animation is interrupted - */ - suspend fun show() { - val targetValue = when { - hasPartiallyExpandedState -> PartiallyExpanded - else -> Expanded - } - animateTo(targetValue) - } - - /** - * Hide the bottom sheet with animation and suspend until it is fully hidden or animation has - * been cancelled. - * @throws [CancellationException] if the animation is interrupted - */ - suspend fun hide() { - check(!skipHiddenState) { - "Attempted to animate to hidden when skipHiddenState was enabled. Set skipHiddenState" + - " to false to use this function." - } - animateTo(Hidden) - } - - /** - * Animate to a [targetValue]. - * If the [targetValue] is not in the set of anchors, the [currentValue] will be updated to the - * [targetValue] without updating the offset. - * - * @throws CancellationException if the interaction interrupted by another interaction like a - * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call. - * - * @param targetValue The target value of the animation - */ - internal suspend fun animateTo( - targetValue: SheetValue, - ) { - anchoredDraggableState.animateTo(targetValue) - } - - /** - * Snap to a [targetValue] without any animation. - * - * @throws CancellationException if the interaction interrupted by another interaction like a - * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call. - * - * @param targetValue The target value of the animation - */ - internal suspend fun snapTo(targetValue: SheetValue) { - anchoredDraggableState.snapTo(targetValue) - } - - /** - * Find the closest anchor taking into account the velocity and settle at it with an animation. - */ - internal suspend fun settle(animationSpec: AnimationSpec) { - anchoredDraggableState.settle(animationSpec) - } - - internal var anchoredDraggableState = androidx.compose.foundation.gestures.AnchoredDraggableState( - initialValue = initialValue, - ) - - internal val offset: Float? get() = anchoredDraggableState.offset - - internal var density: Density? = null - - companion object { - /** - * The default [Saver] implementation for [SheetState]. - */ - @SuppressWarnings("FunctionName") - fun Saver( - skipPartiallyExpanded: Boolean, - density: Density - ) = Saver( - save = { it.currentValue }, - restore = { savedValue -> - CustomSheetState(skipPartiallyExpanded, density, savedValue) - } - ) - } -} From 50e5eef4e311b25d8ce557935dc40417120a14ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 24 Apr 2025 12:20:16 +0200 Subject: [PATCH 05/12] Replace `semantics { invisibleToUser() }` with `hideFromAccessibility()` --- .../impl/timeline/components/TimelineEventTimestampView.kt | 6 +++--- .../impl/timeline/components/TimelineItemEventRow.kt | 4 ++-- .../impl/timeline/components/TimelineItemReactionsView.kt | 4 ++-- .../impl/timeline/components/event/TimelineItemVideoView.kt | 4 ++-- .../components/receipt/TimelineItemReadReceiptView.kt | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt index a29c13ccd5..644cbf2310 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt @@ -18,7 +18,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.invisibleToUser +import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -80,7 +80,7 @@ fun TimelineEventTimestampView( .clickable(isVerifiedUserSendFailure) { eventSink(TimelineEvents.ComputeVerifiedUserSendFailure(event)) } - .semantics { invisibleToUser() } + .semantics { hideFromAccessibility() } ) } @@ -95,7 +95,7 @@ fun TimelineEventTimestampView( .clickable { eventSink(TimelineEvents.ShowShieldDialog(shield)) } - .semantics { invisibleToUser() }, + .semantics { hideFromAccessibility() }, tint = shield.toIconColor(), ) Spacer(modifier = Modifier.width(4.dp)) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index 1c9e8d5c2f..55ee0d7a4f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -39,7 +39,7 @@ import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.clearAndSetSemantics -import androidx.compose.ui.semantics.invisibleToUser +import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.isTraversalGroup import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.traversalIndex @@ -439,7 +439,7 @@ private fun MessageSenderInformation( // Add external clickable modifier with no indicator so the touch target is larger than just the display name .clickable(onClick = onClick, enabled = true, interactionSource = remember { MutableInteractionSource() }, indication = null) .clearAndSetSemantics { - invisibleToUser() + hideFromAccessibility() } ) { Avatar( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt index 02cfa39830..07811b532d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt @@ -16,7 +16,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.invisibleToUser +import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp @@ -43,7 +43,7 @@ fun TimelineItemReactionsView( var expanded: Boolean by rememberSaveable { mutableStateOf(false) } TimelineItemReactionsView( modifier = modifier.semantics { - invisibleToUser() + hideFromAccessibility() }, reactions = reactionsState.reactions, userCanSendReaction = userCanSendReaction, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt index 4ac09f106d..c731005a57 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.invisibleToUser +import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -136,7 +136,7 @@ fun TimelineItemVideoView( imageVector = CompoundIcons.PlaySolid(), contentDescription = stringResource(id = CommonStrings.a11y_play), colorFilter = ColorFilter.tint(Color.White), - modifier = Modifier.semantics { invisibleToUser() } + modifier = Modifier.semantics { hideFromAccessibility() } ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt index e7d80a9d6c..8493bad765 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.invisibleToUser +import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.testTag import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -59,7 +59,7 @@ fun TimelineItemReadReceiptView( if (renderReadReceipts) { ReadReceiptsRow( modifier = modifier.clearAndSetSemantics { - invisibleToUser() + hideFromAccessibility() } ) { ReadReceiptsAvatars( From 4cf1b098e5e6c077328184645210a4cfce50d3c0 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 24 Apr 2025 10:54:20 +0000 Subject: [PATCH 06/12] Update screenshots --- ...components.previews_DatePickerDark_DateTime_pickers_en.png | 4 ++-- ...omponents.previews_DatePickerLight_DateTime_pickers_en.png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en.png index 37b234801f..5555e6bb02 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea4f3ed733159e0413383d8994943b5b7b3100160728138b7014a9567f665021 -size 30632 +oid sha256:3ecb9e2af8ad8221225e71463a1da8589ff1a6c9ba393f636c2dbec08fae513b +size 30991 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en.png index f957752384..897b8d763e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:657c14f3e42751aefbac5fc0b7d94462d2b90da1707ab7ce7997f5db1b29c6d2 -size 31013 +oid sha256:f7f456872a423c0edeb87e5a9fbc7e73896f3cb75609a64c4acb959ffd77e72d +size 31404 From 7fd4e0d35d0ab122b9d8d42f65fe52e965666142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 24 Apr 2025 12:48:24 +0200 Subject: [PATCH 07/12] Add commit and cancel callbacks for autofill on the login view --- .../screens/loginpassword/LoginPasswordView.kt | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt index 2ea63c3955..c72f1a0308 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt @@ -7,6 +7,7 @@ package io.element.android.features.login.impl.screens.loginpassword +import androidx.activity.compose.BackHandler import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -32,6 +33,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.autofill.ContentType import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.platform.LocalAutofillManager import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentType @@ -72,6 +74,13 @@ fun LoginPasswordView( onBackClick: () -> Unit, modifier: Modifier = Modifier, ) { + val autofillManager = LocalAutofillManager.current + + BackHandler { + autofillManager?.cancel() + onBackClick() + } + val isLoading by remember(state.loginAction) { derivedStateOf { state.loginAction is AsyncData.Loading @@ -83,6 +92,8 @@ fun LoginPasswordView( // Clear focus to prevent keyboard issues with textfields focusManager.clearFocus(force = true) + autofillManager?.commit() + state.eventSink(LoginPasswordEvents.Submit) } @@ -91,7 +102,12 @@ fun LoginPasswordView( topBar = { TopAppBar( title = {}, - navigationIcon = { BackButton(onClick = onBackClick) }, + navigationIcon = { + BackButton(onClick = { + autofillManager?.cancel() + onBackClick() + }) + }, ) } ) { padding -> From c350fe1ef694618593612b205d72ea0f3761526b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 24 Apr 2025 16:19:27 +0200 Subject: [PATCH 08/12] Fix broken tests caused mainly by https://issuetracker.google.com/issues/366255137 Add `LocalUiTestMode` composition local and helper functions. --- .../JoinBaseRoomByAddressViewTest.kt | 6 +++- .../timeline/components/MessageEventBubble.kt | 5 ++- .../messages/impl/MessagesViewTest.kt | 4 ++- .../ResolveVerifiedUserSendFailureViewTest.kt | 3 +- .../pinned/list/PinnedMessagesListViewTest.kt | 20 ++++++----- .../impl/timeline/TimelineViewTest.kt | 34 +++++++++++-------- .../RolesAndPermissionsViewTest.kt | 26 ++++++++------ .../roomlist/impl/RoomListContextMenuTest.kt | 11 +++--- .../roomlist/impl/RoomListViewTest.kt | 32 +++++++++-------- libraries/designsystem/build.gradle.kts | 1 + .../theme/components/ModalBottomSheet.kt | 7 +++- .../designsystem/utils/LocalUiTestMode.kt | 33 ++++++++++++++++++ .../MediaDeleteConfirmationBottomSheetTest.kt | 3 +- .../details/MediaDetailsBottomSheetTest.kt | 3 +- .../impl/viewer/MediaViewerViewTest.kt | 3 +- tools/detekt/detekt.yml | 1 + 16 files changed, 131 insertions(+), 61 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LocalUiTestMode.kt diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt index 93db738d35..e2842f29bc 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt @@ -8,12 +8,14 @@ package io.element.android.features.createroom.impl.joinbyaddress import androidx.activity.ComponentActivity +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.createroom.impl.R +import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn @@ -57,6 +59,8 @@ private fun AndroidComposeTestRule.setJoinR state: JoinRoomByAddressState, ) { setContent { - JoinRoomByAddressView(state = state) + CompositionLocalProvider(LocalUiTestMode provides true) { + JoinRoomByAddressView(state = state) + } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt index ad5337031c..2b48a99dcd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt @@ -47,6 +47,7 @@ import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.messageFromMeBackground import io.element.android.libraries.designsystem.theme.messageFromOtherBackground +import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.utils.time.isTalkbackActive @@ -112,7 +113,9 @@ fun MessageEventBubble( state.isMine -> ElementTheme.colors.messageFromMeBackground else -> ElementTheme.colors.messageFromOtherBackground } - val bubbleShape = bubbleShape() + // If we're running in UI test mode, we want to use a different shape to avoid + // this issue: https://issuetracker.google.com/issues/366255137 + val bubbleShape = if (LocalUiTestMode.current) RoundedCornerShape(12.dp) else bubbleShape() val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx() val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx() val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt index 9d378f7de7..a78f2ff90b 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -52,6 +52,7 @@ import io.element.android.features.messages.impl.timeline.components.receipt.aRe import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent +import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName @@ -570,7 +571,8 @@ private fun AndroidComposeTestRule.setMessa setContent { // Cannot use the RichTextEditor, so simulate a LocalInspectionMode CompositionLocalProvider( - LocalInspectionMode provides true + LocalInspectionMode provides true, + LocalUiTestMode provides true, ) { MessagesView( state = state, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt index a8190e8776..439cd6870f 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt @@ -11,6 +11,7 @@ import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.designsystem.utils.setContentForUiTest import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn @@ -54,7 +55,7 @@ class ResolveVerifiedUserSendFailureViewTest { private fun AndroidComposeTestRule.setResolveVerifiedUserSendFailureView( state: ResolveVerifiedUserSendFailureState, ) { - setContent { + setContentForUiTest { ResolveVerifiedUserSendFailureView(state = state) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt index 4d66365a58..3fe42747bb 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt @@ -8,6 +8,7 @@ package io.element.android.features.messages.impl.pinned.list import androidx.activity.ComponentActivity +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.longClick @@ -22,6 +23,7 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemList import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder @@ -104,13 +106,15 @@ private fun AndroidComposeTestRule.setPinne onLinkLongClick: (Link) -> Unit = EnsureNeverCalledWithParam(), ) { setSafeContent { - PinnedMessagesListView( - state = state, - onBackClick = onBackClick, - onEventClick = onEventClick, - onUserDataClick = onUserDataClick, - onLinkClick = onLinkClick, - onLinkLongClick = onLinkLongClick, - ) + CompositionLocalProvider(LocalUiTestMode provides true) { + PinnedMessagesListView( + state = state, + onBackClick = onBackClick, + onEventClick = onEventClick, + onUserDataClick = onUserDataClick, + onLinkClick = onLinkClick, + onLinkLongClick = onLinkLongClick, + ) + } } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt index 110545dcae..3db195d23b 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -8,6 +8,7 @@ package io.element.android.features.messages.impl.timeline import androidx.activity.ComponentActivity +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription @@ -22,6 +23,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState +import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.timeline.Timeline @@ -188,20 +190,22 @@ private fun AndroidComposeTestRule.setTimel forceJumpToBottomVisibility: Boolean = false, ) { setSafeContent { - TimelineView( - state = state, - timelineProtectionState = timelineProtectionState, - onUserDataClick = onUserDataClick, - onLinkClick = onLinkClick, - onContentClick = onMessageClick, - onMessageLongClick = onMessageLongClick, - onSwipeToReply = onSwipeToReply, - onReactionClick = onReactionClick, - onReactionLongClick = onReactionLongClick, - onMoreReactionsClick = onMoreReactionsClick, - onReadReceiptClick = onReadReceiptClick, - onJoinCallClick = onJoinCallClick, - forceJumpToBottomVisibility = forceJumpToBottomVisibility, - ) + CompositionLocalProvider(LocalUiTestMode provides true) { + TimelineView( + state = state, + timelineProtectionState = timelineProtectionState, + onUserDataClick = onUserDataClick, + onLinkClick = onLinkClick, + onContentClick = onMessageClick, + onMessageLongClick = onMessageLongClick, + onSwipeToReply = onSwipeToReply, + onReactionClick = onReactionClick, + onReactionLongClick = onReactionLongClick, + onMoreReactionsClick = onMoreReactionsClick, + onReadReceiptClick = onReadReceiptClick, + onJoinCallClick = onJoinCallClick, + forceJumpToBottomVisibility = forceJumpToBottomVisibility, + ) + } } } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsViewTest.kt index 9d793952b3..013836f001 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsViewTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsViewTest.kt @@ -8,11 +8,13 @@ package io.element.android.features.roomdetails.impl.rolesandpermissions import androidx.activity.ComponentActivity +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.roomdetails.impl.R import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -167,16 +169,18 @@ private fun AndroidComposeTestRule.setRoles openPermissionScreens: () -> Unit = EnsureNeverCalled(), ) { setContent { - RolesAndPermissionsView( - state = state, - rolesAndPermissionsNavigator = object : RolesAndPermissionsNavigator { - override fun onBackClick() = goBack() - override fun openAdminList() = openAdminList() - override fun openModeratorList() = openModeratorList() - override fun openEditRoomDetailsPermissions() = openPermissionScreens() - override fun openModerationPermissions() = openPermissionScreens() - override fun openMessagesAndContentPermissions() = openPermissionScreens() - } - ) + CompositionLocalProvider(LocalUiTestMode provides true) { + RolesAndPermissionsView( + state = state, + rolesAndPermissionsNavigator = object : RolesAndPermissionsNavigator { + override fun onBackClick() = goBack() + override fun openAdminList() = openAdminList() + override fun openModeratorList() = openModeratorList() + override fun openEditRoomDetailsPermissions() = openPermissionScreens() + override fun openModerationPermissions() = openPermissionScreens() + override fun openMessagesAndContentPermissions() = openPermissionScreens() + } + ) + } } } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt index 80b5a87c10..d85b835df3 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt @@ -10,6 +10,7 @@ package io.element.android.features.roomlist.impl import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.designsystem.utils.setContentForUiTest import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureCalledOnceWithParam @@ -28,7 +29,7 @@ class RoomListContextMenuTest { fun `clicking on Mark as read generates expected Events`() { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown(hasNewContent = true) - rule.setContent { + rule.setContentForUiTest { RoomListContextMenu( contextMenu = contextMenu, canReportRoom = false, @@ -50,7 +51,7 @@ class RoomListContextMenuTest { fun `clicking on Mark as unread generates expected Events`() { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown(hasNewContent = false) - rule.setContent { + rule.setContentForUiTest { RoomListContextMenu( contextMenu = contextMenu, canReportRoom = false, @@ -72,7 +73,7 @@ class RoomListContextMenuTest { fun `clicking on Leave room generates expected Events`() { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown(isDm = false) - rule.setContent { + rule.setContentForUiTest { RoomListContextMenu( contextMenu = contextMenu, canReportRoom = false, @@ -114,7 +115,7 @@ class RoomListContextMenuTest { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown() val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit) - rule.setContent { + rule.setContentForUiTest { RoomListContextMenu( contextMenu = contextMenu, canReportRoom = false, @@ -133,7 +134,7 @@ class RoomListContextMenuTest { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown(isDm = false, isFavorite = false) val callback = EnsureNeverCalledWithParam() - rule.setContent { + rule.setContentForUiTest { RoomListContextMenu( contextMenu = contextMenu, canReportRoom = false, diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt index 0242591f55..0fa0f751c5 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt @@ -10,6 +10,7 @@ package io.element.android.features.roomlist.impl import androidx.activity.ComponentActivity +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.longClick @@ -21,6 +22,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.roomlist.impl.components.RoomListMenuAction import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType +import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -52,7 +54,7 @@ class RoomListViewTest { eventsRecorder.assertList( listOf( RoomListEvents.UpdateVisibleRange(IntRange.EMPTY), - RoomListEvents.UpdateVisibleRange(0 until 2), + RoomListEvents.UpdateVisibleRange(0..2), ) ) } @@ -274,18 +276,20 @@ private fun AndroidComposeTestRule.setRoomL onDeclineInviteAndBlockUser: (RoomListRoomSummary) -> Unit = EnsureNeverCalledWithParam(), ) { setContent { - RoomListView( - state = state, - onRoomClick = onRoomClick, - onSettingsClick = onSettingsClick, - onSetUpRecoveryClick = onSetUpRecoveryClick, - onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, - onCreateRoomClick = onCreateRoomClick, - onRoomSettingsClick = onRoomSettingsClick, - onMenuActionClick = onMenuActionClick, - onDeclineInviteAndBlockUser = onDeclineInviteAndBlockUser, - onReportRoomClick = onReportRoomClick, - acceptDeclineInviteView = { }, - ) + CompositionLocalProvider(LocalUiTestMode provides true) { + RoomListView( + state = state, + onRoomClick = onRoomClick, + onSettingsClick = onSettingsClick, + onSetUpRecoveryClick = onSetUpRecoveryClick, + onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, + onCreateRoomClick = onCreateRoomClick, + onRoomSettingsClick = onRoomSettingsClick, + onMenuActionClick = onMenuActionClick, + onDeclineInviteAndBlockUser = onDeclineInviteAndBlockUser, + onReportRoomClick = onReportRoomClick, + acceptDeclineInviteView = { }, + ) + } } } diff --git a/libraries/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts index d95566ebb5..e18e045058 100644 --- a/libraries/designsystem/build.gradle.kts +++ b/libraries/designsystem/build.gradle.kts @@ -27,6 +27,7 @@ android { dependencies { api(libs.compound) + implementation(libs.androidx.compose.ui.test.junit) implementation(libs.androidx.compose.material3.windowsizeclass) implementation(libs.androidx.compose.material3.adaptive) implementation(libs.coil.compose) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt index 51a6cd9ee1..e0d9c749ec 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme @@ -33,6 +34,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.PreviewGroup import io.element.android.libraries.designsystem.preview.sheetStateForPreview +import io.element.android.libraries.designsystem.utils.LocalUiTestMode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -52,11 +54,14 @@ fun ModalBottomSheet( content: @Composable ColumnScope.() -> Unit, ) { val safeSheetState = if (LocalInspectionMode.current) sheetStateForPreview() else sheetState + // If we're running in UI test mode, we want to use a different shape to avoid + // this issue: https://issuetracker.google.com/issues/366255137 + val safeShape = if (LocalUiTestMode.current) RoundedCornerShape(12.dp) else shape androidx.compose.material3.ModalBottomSheet( onDismissRequest = onDismissRequest, modifier = modifier, sheetState = safeSheetState, - shape = shape, + shape = safeShape, containerColor = containerColor, contentColor = contentColor, tonalElevation = tonalElevation, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LocalUiTestMode.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LocalUiTestMode.kt new file mode 100644 index 0000000000..5609d89e04 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LocalUiTestMode.kt @@ -0,0 +1,33 @@ +/* + * Copyright 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.libraries.designsystem.utils + +import androidx.activity.ComponentActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import org.junit.rules.TestRule + +/** + * A composition local that indicates whether the app is running in UI test mode. + */ +val LocalUiTestMode = staticCompositionLocalOf { false } + +/** + * Sets the UI testing mode as enabled. + * + * This is used for working around issues like https://issuetracker.google.com/issues/366255137. + */ +fun AndroidComposeTestRule.setContentForUiTest( + block: @Composable () -> Unit, +) { + setContent { + CompositionLocalProvider(LocalUiTestMode provides true, block) + } +} diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt index c71cdb8573..4ac7c2a4d4 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.designsystem.utils.setContentForUiTest import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -60,7 +61,7 @@ private fun AndroidComposeTestRule.setMedia onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(), onDismiss: () -> Unit = EnsureNeverCalled(), ) { - setContent { + setContentForUiTest { MediaDeleteConfirmationBottomSheet( state = state, onDelete = onDelete, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt index b74ae07b8b..9c2be23e0e 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.designsystem.utils.setContentForUiTest import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -100,7 +101,7 @@ private fun AndroidComposeTestRule.setMedia onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(), onDismiss: () -> Unit = EnsureNeverCalled(), ) { - setContent { + setContentForUiTest { MediaDetailsBottomSheet( state = state, onViewInTimeline = onViewInTimeline, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt index ba9881a786..bb42109bf3 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeDown import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.utils.setContentForUiTest import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia import io.element.android.libraries.ui.strings.CommonStrings @@ -249,7 +250,7 @@ private fun AndroidComposeTestRule.setMedia state: MediaViewerState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { - setContent { + setContentForUiTest { MediaViewerView( state = state, audioFocus = null, diff --git a/tools/detekt/detekt.yml b/tools/detekt/detekt.yml index 71187feced..9af20180c6 100644 --- a/tools/detekt/detekt.yml +++ b/tools/detekt/detekt.yml @@ -230,6 +230,7 @@ Compose: - LocalMentionSpanUpdater - LocalAnalyticsService - LocalBuildMeta + - LocalUiTestMode CompositionLocalNaming: active: true ContentEmitterReturningValues: From 2aefe2ffaad311f0e5510621cfa22c4aa8621c1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 24 Apr 2025 16:52:04 +0200 Subject: [PATCH 09/12] Remove dependency that caused a new license to need to be approved --- .../ResolveVerifiedUserSendFailureViewTest.kt | 9 +- .../roomlist/impl/RoomListContextMenuTest.kt | 107 +++++++++--------- libraries/designsystem/build.gradle.kts | 1 - .../designsystem/utils/LocalUiTestMode.kt | 18 --- .../MediaDeleteConfirmationBottomSheetTest.kt | 17 +-- .../details/MediaDetailsBottomSheetTest.kt | 23 ++-- .../impl/viewer/MediaViewerViewTest.kt | 19 ++-- 7 files changed, 92 insertions(+), 102 deletions(-) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt index 439cd6870f..a17ff00d7e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt @@ -8,10 +8,11 @@ package io.element.android.features.messages.impl.crypto.sendfailure.resolve import androidx.activity.ComponentActivity +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.libraries.designsystem.utils.setContentForUiTest +import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn @@ -55,8 +56,10 @@ class ResolveVerifiedUserSendFailureViewTest { private fun AndroidComposeTestRule.setResolveVerifiedUserSendFailureView( state: ResolveVerifiedUserSendFailureState, ) { - setContentForUiTest { - ResolveVerifiedUserSendFailureView(state = state) + setContent { + CompositionLocalProvider(LocalUiTestMode provides true) { + ResolveVerifiedUserSendFailureView(state = state) + } } } } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt index d85b835df3..ec38d547cc 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt @@ -8,9 +8,11 @@ package io.element.android.features.roomlist.impl import androidx.activity.ComponentActivity +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.libraries.designsystem.utils.setContentForUiTest +import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureCalledOnceWithParam @@ -29,15 +31,10 @@ class RoomListContextMenuTest { fun `clicking on Mark as read generates expected Events`() { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown(hasNewContent = true) - rule.setContentForUiTest { - RoomListContextMenu( - contextMenu = contextMenu, - canReportRoom = false, - eventSink = eventsRecorder, - onRoomSettingsClick = EnsureNeverCalledWithParam(), - onReportRoomClick = EnsureNeverCalledWithParam(), - ) - } + rule.setRoomListContextMenu( + contextMenu = contextMenu, + eventSink = eventsRecorder, + ) rule.clickOn(R.string.screen_roomlist_mark_as_read) eventsRecorder.assertList( listOf( @@ -51,15 +48,10 @@ class RoomListContextMenuTest { fun `clicking on Mark as unread generates expected Events`() { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown(hasNewContent = false) - rule.setContentForUiTest { - RoomListContextMenu( - contextMenu = contextMenu, - canReportRoom = false, - eventSink = eventsRecorder, - onRoomSettingsClick = EnsureNeverCalledWithParam(), - onReportRoomClick = EnsureNeverCalledWithParam(), - ) - } + rule.setRoomListContextMenu( + contextMenu = contextMenu, + eventSink = eventsRecorder, + ) rule.clickOn(R.string.screen_roomlist_mark_as_unread) eventsRecorder.assertList( listOf( @@ -73,15 +65,10 @@ class RoomListContextMenuTest { fun `clicking on Leave room generates expected Events`() { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown(isDm = false) - rule.setContentForUiTest { - RoomListContextMenu( - contextMenu = contextMenu, - canReportRoom = false, - eventSink = eventsRecorder, - onRoomSettingsClick = EnsureNeverCalledWithParam(), - onReportRoomClick = EnsureNeverCalledWithParam(), - ) - } + rule.setRoomListContextMenu( + contextMenu = contextMenu, + eventSink = eventsRecorder, + ) rule.clickOn(CommonStrings.action_leave_room) eventsRecorder.assertList( listOf( @@ -96,15 +83,13 @@ class RoomListContextMenuTest { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown() val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit) - rule.setContent { - RoomListContextMenu( - contextMenu = contextMenu, - canReportRoom = true, - eventSink = eventsRecorder, - onRoomSettingsClick = EnsureNeverCalledWithParam(), - onReportRoomClick = callback, - ) - } + rule.setRoomListContextMenu( + contextMenu = contextMenu, + canReportRoom = true, + eventSink = eventsRecorder, + onRoomSettingsClick = EnsureNeverCalledWithParam(), + onReportRoomClick = callback, + ) rule.clickOn(CommonStrings.action_report_room) eventsRecorder.assertSingle(RoomListEvents.HideContextMenu) callback.assertSuccess() @@ -115,15 +100,11 @@ class RoomListContextMenuTest { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown() val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit) - rule.setContentForUiTest { - RoomListContextMenu( - contextMenu = contextMenu, - canReportRoom = false, - eventSink = eventsRecorder, - onRoomSettingsClick = callback, - onReportRoomClick = EnsureNeverCalledWithParam(), - ) - } + rule.setRoomListContextMenu( + contextMenu = contextMenu, + eventSink = eventsRecorder, + onRoomSettingsClick = callback, + ) rule.clickOn(CommonStrings.common_settings) eventsRecorder.assertSingle(RoomListEvents.HideContextMenu) callback.assertSuccess() @@ -134,15 +115,11 @@ class RoomListContextMenuTest { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown(isDm = false, isFavorite = false) val callback = EnsureNeverCalledWithParam() - rule.setContentForUiTest { - RoomListContextMenu( - contextMenu = contextMenu, - canReportRoom = false, - eventSink = eventsRecorder, - onRoomSettingsClick = callback, - onReportRoomClick = EnsureNeverCalledWithParam(), - ) - } + rule.setRoomListContextMenu( + contextMenu = contextMenu, + eventSink = eventsRecorder, + onRoomSettingsClick = callback, + ) rule.clickOn(CommonStrings.common_favourite) eventsRecorder.assertList( listOf( @@ -150,4 +127,24 @@ class RoomListContextMenuTest { ) ) } + + private fun AndroidComposeTestRule<*, *>.setRoomListContextMenu( + contextMenu: RoomListState.ContextMenu.Shown, + canReportRoom: Boolean = false, + eventSink: (RoomListEvents) -> Unit, + onRoomSettingsClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(), + onReportRoomClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(), + ) { + setContent { + CompositionLocalProvider(LocalUiTestMode provides true) { + RoomListContextMenu( + contextMenu = contextMenu, + canReportRoom = canReportRoom, + onRoomSettingsClick = onRoomSettingsClick, + onReportRoomClick = onReportRoomClick, + eventSink = eventSink, + ) + } + } + } } diff --git a/libraries/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts index e18e045058..d95566ebb5 100644 --- a/libraries/designsystem/build.gradle.kts +++ b/libraries/designsystem/build.gradle.kts @@ -27,7 +27,6 @@ android { dependencies { api(libs.compound) - implementation(libs.androidx.compose.ui.test.junit) implementation(libs.androidx.compose.material3.windowsizeclass) implementation(libs.androidx.compose.material3.adaptive) implementation(libs.coil.compose) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LocalUiTestMode.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LocalUiTestMode.kt index 5609d89e04..5c1288f0e7 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LocalUiTestMode.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LocalUiTestMode.kt @@ -7,27 +7,9 @@ package io.element.android.libraries.designsystem.utils -import androidx.activity.ComponentActivity -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import org.junit.rules.TestRule /** * A composition local that indicates whether the app is running in UI test mode. */ val LocalUiTestMode = staticCompositionLocalOf { false } - -/** - * Sets the UI testing mode as enabled. - * - * This is used for working around issues like https://issuetracker.google.com/issues/366255137. - */ -fun AndroidComposeTestRule.setContentForUiTest( - block: @Composable () -> Unit, -) { - setContent { - CompositionLocalProvider(LocalUiTestMode provides true, block) - } -} diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt index 4ac7c2a4d4..56d9ab4915 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt @@ -8,11 +8,12 @@ package io.element.android.libraries.mediaviewer.impl.details import androidx.activity.ComponentActivity +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.libraries.designsystem.utils.setContentForUiTest +import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -61,11 +62,13 @@ private fun AndroidComposeTestRule.setMedia onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(), onDismiss: () -> Unit = EnsureNeverCalled(), ) { - setContentForUiTest { - MediaDeleteConfirmationBottomSheet( - state = state, - onDelete = onDelete, - onDismiss = onDismiss, - ) + setContent { + CompositionLocalProvider(LocalUiTestMode provides true) { + MediaDeleteConfirmationBottomSheet( + state = state, + onDelete = onDelete, + onDismiss = onDismiss, + ) + } } } diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt index 9c2be23e0e..7a8c414a9c 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt @@ -8,11 +8,12 @@ package io.element.android.libraries.mediaviewer.impl.details import androidx.activity.ComponentActivity +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.libraries.designsystem.utils.setContentForUiTest +import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -101,14 +102,16 @@ private fun AndroidComposeTestRule.setMedia onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(), onDismiss: () -> Unit = EnsureNeverCalled(), ) { - setContentForUiTest { - MediaDetailsBottomSheet( - state = state, - onViewInTimeline = onViewInTimeline, - onShare = onShare, - onDownload = onDownload, - onDelete = onDelete, - onDismiss = onDismiss, - ) + setContent { + CompositionLocalProvider(LocalUiTestMode provides true) { + MediaDetailsBottomSheet( + state = state, + onViewInTimeline = onViewInTimeline, + onShare = onShare, + onDownload = onDownload, + onDelete = onDelete, + onDismiss = onDismiss, + ) + } } } diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt index bb42109bf3..56e217bd13 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.mediaviewer.impl.viewer import android.net.Uri import androidx.activity.ComponentActivity +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule @@ -18,7 +19,7 @@ import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeDown import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.designsystem.utils.setContentForUiTest +import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia import io.element.android.libraries.ui.strings.CommonStrings @@ -250,12 +251,14 @@ private fun AndroidComposeTestRule.setMedia state: MediaViewerState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { - setContentForUiTest { - MediaViewerView( - state = state, - audioFocus = null, - textFileViewer = { _, _ -> }, - onBackClick = onBackClick, - ) + setContent { + CompositionLocalProvider(LocalUiTestMode provides true) { + MediaViewerView( + state = state, + audioFocus = null, + textFileViewer = { _, _ -> }, + onBackClick = onBackClick, + ) + } } } From cec524d4ccce46923f99dc2f807791a6d11d4ff2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 3 Jun 2025 17:18:57 +0200 Subject: [PATCH 10/12] Let setSafeContent handle setting the value for LocalUiTestMode --- .../JoinBaseRoomByAddressViewTest.kt | 9 ++--- .../messages/impl/MessagesViewTest.kt | 9 ++--- .../ResolveVerifiedUserSendFailureViewTest.kt | 9 ++--- .../pinned/list/PinnedMessagesListViewTest.kt | 22 +++++------- .../impl/timeline/TimelineViewTest.kt | 36 +++++++++---------- .../RolesAndPermissionsViewTest.kt | 31 ++++++++-------- .../roomlist/impl/RoomListContextMenuTest.kt | 21 +++++------ .../roomlist/impl/RoomListViewTest.kt | 33 ++++++++--------- .../MediaDeleteConfirmationBottomSheetTest.kt | 17 ++++----- .../details/MediaDetailsBottomSheetTest.kt | 23 ++++++------ .../impl/viewer/MediaViewerViewTest.kt | 19 +++++----- tests/testutils/build.gradle.kts | 1 + .../testutils/RobolectricDispatcherCleaner.kt | 17 +++++++-- 13 files changed, 112 insertions(+), 135 deletions(-) diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt index e2842f29bc..b2e75c9f5a 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt @@ -8,17 +8,16 @@ package io.element.android.features.createroom.impl.joinbyaddress import androidx.activity.ComponentActivity -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.createroom.impl.R -import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.setSafeContent import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule @@ -58,9 +57,7 @@ class JoinBaseRoomByAddressViewTest { private fun AndroidComposeTestRule.setJoinRoomByAddressView( state: JoinRoomByAddressState, ) { - setContent { - CompositionLocalProvider(LocalUiTestMode provides true) { - JoinRoomByAddressView(state = state) - } + setSafeContent { + JoinRoomByAddressView(state = state) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt index a78f2ff90b..4f6ac91890 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -52,7 +52,6 @@ import io.element.android.features.messages.impl.timeline.components.receipt.aRe import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent -import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName @@ -69,6 +68,7 @@ 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.setSafeContent import kotlinx.collections.immutable.persistentListOf import org.junit.Rule import org.junit.Test @@ -568,12 +568,9 @@ private fun AndroidComposeTestRule.setMessa onJoinCallClick: () -> Unit = EnsureNeverCalled(), onViewAllPinnedMessagesClick: () -> Unit = EnsureNeverCalled(), ) { - setContent { + setSafeContent { // Cannot use the RichTextEditor, so simulate a LocalInspectionMode - CompositionLocalProvider( - LocalInspectionMode provides true, - LocalUiTestMode provides true, - ) { + CompositionLocalProvider(LocalInspectionMode provides true) { MessagesView( state = state, onBackClick = onBackClick, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt index a17ff00d7e..6a83437758 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt @@ -8,14 +8,13 @@ package io.element.android.features.messages.impl.crypto.sendfailure.resolve import androidx.activity.ComponentActivity -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.setSafeContent import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule @@ -56,10 +55,8 @@ class ResolveVerifiedUserSendFailureViewTest { private fun AndroidComposeTestRule.setResolveVerifiedUserSendFailureView( state: ResolveVerifiedUserSendFailureState, ) { - setContent { - CompositionLocalProvider(LocalUiTestMode provides true) { - ResolveVerifiedUserSendFailureView(state = state) - } + setSafeContent { + ResolveVerifiedUserSendFailureView(state = state) } } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt index 3fe42747bb..5e9ad97f87 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt @@ -8,7 +8,6 @@ package io.element.android.features.messages.impl.pinned.list import androidx.activity.ComponentActivity -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.longClick @@ -23,7 +22,6 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemList import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent import io.element.android.libraries.matrix.api.user.MatrixUser -import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder @@ -105,16 +103,14 @@ private fun AndroidComposeTestRule.setPinne onLinkClick: (Link) -> Unit = EnsureNeverCalledWithParam(), onLinkLongClick: (Link) -> Unit = EnsureNeverCalledWithParam(), ) { - setSafeContent { - CompositionLocalProvider(LocalUiTestMode provides true) { - PinnedMessagesListView( - state = state, - onBackClick = onBackClick, - onEventClick = onEventClick, - onUserDataClick = onUserDataClick, - onLinkClick = onLinkClick, - onLinkLongClick = onLinkLongClick, - ) - } + setSafeContent(clearAndroidUiDispatcher = true) { + PinnedMessagesListView( + state = state, + onBackClick = onBackClick, + onEventClick = onEventClick, + onUserDataClick = onUserDataClick, + onLinkClick = onLinkClick, + onLinkLongClick = onLinkLongClick, + ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt index 3db195d23b..c4a2351990 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -8,7 +8,6 @@ package io.element.android.features.messages.impl.timeline import androidx.activity.ComponentActivity -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription @@ -23,7 +22,6 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState -import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.timeline.Timeline @@ -189,23 +187,21 @@ private fun AndroidComposeTestRule.setTimel onJoinCallClick: () -> Unit = EnsureNeverCalled(), forceJumpToBottomVisibility: Boolean = false, ) { - setSafeContent { - CompositionLocalProvider(LocalUiTestMode provides true) { - TimelineView( - state = state, - timelineProtectionState = timelineProtectionState, - onUserDataClick = onUserDataClick, - onLinkClick = onLinkClick, - onContentClick = onMessageClick, - onMessageLongClick = onMessageLongClick, - onSwipeToReply = onSwipeToReply, - onReactionClick = onReactionClick, - onReactionLongClick = onReactionLongClick, - onMoreReactionsClick = onMoreReactionsClick, - onReadReceiptClick = onReadReceiptClick, - onJoinCallClick = onJoinCallClick, - forceJumpToBottomVisibility = forceJumpToBottomVisibility, - ) - } + setSafeContent(clearAndroidUiDispatcher = true) { + TimelineView( + state = state, + timelineProtectionState = timelineProtectionState, + onUserDataClick = onUserDataClick, + onLinkClick = onLinkClick, + onContentClick = onMessageClick, + onMessageLongClick = onMessageLongClick, + onSwipeToReply = onSwipeToReply, + onReactionClick = onReactionClick, + onReactionLongClick = onReactionLongClick, + onMoreReactionsClick = onMoreReactionsClick, + onReadReceiptClick = onReadReceiptClick, + onJoinCallClick = onJoinCallClick, + forceJumpToBottomVisibility = forceJumpToBottomVisibility, + ) } } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsViewTest.kt index 013836f001..8aded9f698 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsViewTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsViewTest.kt @@ -8,13 +8,11 @@ package io.element.android.features.roomdetails.impl.rolesandpermissions import androidx.activity.ComponentActivity -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.roomdetails.impl.R import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -23,6 +21,7 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledTimes import io.element.android.tests.testutils.pressBack +import io.element.android.tests.testutils.setSafeContent import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -161,26 +160,24 @@ class RolesAndPermissionsViewTest { private fun AndroidComposeTestRule.setRolesAndPermissionsView( state: RolesAndPermissionsState = aRolesAndPermissionsState( - eventSink = EventsRecorder(expectEvents = false), + eventSink = EventsRecorder(expectEvents = false), ), goBack: () -> Unit = EnsureNeverCalled(), openAdminList: () -> Unit = EnsureNeverCalled(), openModeratorList: () -> Unit = EnsureNeverCalled(), openPermissionScreens: () -> Unit = EnsureNeverCalled(), ) { - setContent { - CompositionLocalProvider(LocalUiTestMode provides true) { - RolesAndPermissionsView( - state = state, - rolesAndPermissionsNavigator = object : RolesAndPermissionsNavigator { - override fun onBackClick() = goBack() - override fun openAdminList() = openAdminList() - override fun openModeratorList() = openModeratorList() - override fun openEditRoomDetailsPermissions() = openPermissionScreens() - override fun openModerationPermissions() = openPermissionScreens() - override fun openMessagesAndContentPermissions() = openPermissionScreens() - } - ) - } + setSafeContent { + RolesAndPermissionsView( + state = state, + rolesAndPermissionsNavigator = object : RolesAndPermissionsNavigator { + override fun onBackClick() = goBack() + override fun openAdminList() = openAdminList() + override fun openModeratorList() = openModeratorList() + override fun openEditRoomDetailsPermissions() = openPermissionScreens() + override fun openModerationPermissions() = openPermissionScreens() + override fun openMessagesAndContentPermissions() = openPermissionScreens() + } + ) } } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt index ec38d547cc..6becb39e7a 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt @@ -8,17 +8,16 @@ package io.element.android.features.roomlist.impl import androidx.activity.ComponentActivity -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureCalledOnceWithParam import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.setSafeContent import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -135,16 +134,14 @@ class RoomListContextMenuTest { onRoomSettingsClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onReportRoomClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(), ) { - setContent { - CompositionLocalProvider(LocalUiTestMode provides true) { - RoomListContextMenu( - contextMenu = contextMenu, - canReportRoom = canReportRoom, - onRoomSettingsClick = onRoomSettingsClick, - onReportRoomClick = onReportRoomClick, - eventSink = eventSink, - ) - } + setSafeContent { + RoomListContextMenu( + contextMenu = contextMenu, + canReportRoom = canReportRoom, + onRoomSettingsClick = onRoomSettingsClick, + onReportRoomClick = onReportRoomClick, + eventSink = eventSink, + ) } } } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt index 0fa0f751c5..299ef07578 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt @@ -10,7 +10,6 @@ package io.element.android.features.roomlist.impl import androidx.activity.ComponentActivity -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.longClick @@ -22,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.roomlist.impl.components.RoomListMenuAction import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType -import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -31,6 +29,7 @@ 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.ensureCalledOnceWithParam +import io.element.android.tests.testutils.setSafeContent import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Rule import org.junit.Test @@ -275,21 +274,19 @@ private fun AndroidComposeTestRule.setRoomL onReportRoomClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onDeclineInviteAndBlockUser: (RoomListRoomSummary) -> Unit = EnsureNeverCalledWithParam(), ) { - setContent { - CompositionLocalProvider(LocalUiTestMode provides true) { - RoomListView( - state = state, - onRoomClick = onRoomClick, - onSettingsClick = onSettingsClick, - onSetUpRecoveryClick = onSetUpRecoveryClick, - onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, - onCreateRoomClick = onCreateRoomClick, - onRoomSettingsClick = onRoomSettingsClick, - onMenuActionClick = onMenuActionClick, - onDeclineInviteAndBlockUser = onDeclineInviteAndBlockUser, - onReportRoomClick = onReportRoomClick, - acceptDeclineInviteView = { }, - ) - } + setSafeContent { + RoomListView( + state = state, + onRoomClick = onRoomClick, + onSettingsClick = onSettingsClick, + onSetUpRecoveryClick = onSetUpRecoveryClick, + onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, + onCreateRoomClick = onCreateRoomClick, + onRoomSettingsClick = onRoomSettingsClick, + onMenuActionClick = onMenuActionClick, + onDeclineInviteAndBlockUser = onDeclineInviteAndBlockUser, + onReportRoomClick = onReportRoomClick, + acceptDeclineInviteView = { }, + ) } } diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt index 56d9ab4915..1596104c46 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt @@ -8,12 +8,10 @@ package io.element.android.libraries.mediaviewer.impl.details import androidx.activity.ComponentActivity -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -21,6 +19,7 @@ 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.setSafeContent import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule @@ -62,13 +61,11 @@ private fun AndroidComposeTestRule.setMedia onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(), onDismiss: () -> Unit = EnsureNeverCalled(), ) { - setContent { - CompositionLocalProvider(LocalUiTestMode provides true) { - MediaDeleteConfirmationBottomSheet( - state = state, - onDelete = onDelete, - onDismiss = onDismiss, - ) - } + setSafeContent { + MediaDeleteConfirmationBottomSheet( + state = state, + onDelete = onDelete, + onDismiss = onDismiss, + ) } } diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt index 7a8c414a9c..1289d73672 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt @@ -8,18 +8,17 @@ package io.element.android.libraries.mediaviewer.impl.details import androidx.activity.ComponentActivity -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.matrix.api.core.EventId 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.ensureCalledOnceWithParam +import io.element.android.tests.testutils.setSafeContent import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule @@ -102,16 +101,14 @@ private fun AndroidComposeTestRule.setMedia onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(), onDismiss: () -> Unit = EnsureNeverCalled(), ) { - setContent { - CompositionLocalProvider(LocalUiTestMode provides true) { - MediaDetailsBottomSheet( - state = state, - onViewInTimeline = onViewInTimeline, - onShare = onShare, - onDownload = onDownload, - onDelete = onDelete, - onDismiss = onDismiss, - ) - } + setSafeContent { + MediaDetailsBottomSheet( + state = state, + onViewInTimeline = onViewInTimeline, + onShare = onShare, + onDownload = onDownload, + onDelete = onDelete, + onDismiss = onDismiss, + ) } } diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt index 56e217bd13..74d83b733b 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt @@ -9,7 +9,6 @@ package io.element.android.libraries.mediaviewer.impl.viewer import android.net.Uri import androidx.activity.ComponentActivity -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule @@ -19,7 +18,6 @@ import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeDown import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia import io.element.android.libraries.ui.strings.CommonStrings @@ -28,6 +26,7 @@ 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.setSafeContent import io.mockk.mockk import org.junit.Rule import org.junit.Test @@ -251,14 +250,12 @@ private fun AndroidComposeTestRule.setMedia state: MediaViewerState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { - setContent { - CompositionLocalProvider(LocalUiTestMode provides true) { - MediaViewerView( - state = state, - audioFocus = null, - textFileViewer = { _, _ -> }, - onBackClick = onBackClick, - ) - } + setSafeContent { + MediaViewerView( + state = state, + audioFocus = null, + textFileViewer = { _, _ -> }, + onBackClick = onBackClick, + ) } } diff --git a/tests/testutils/build.gradle.kts b/tests/testutils/build.gradle.kts index a7b81808f9..8a7730095a 100644 --- a/tests/testutils/build.gradle.kts +++ b/tests/testutils/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) implementation(projects.libraries.core) + implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) implementation(projects.services.toolbox.api) implementation(libs.test.turbine) diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt index 0723d85d65..620b5def40 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt @@ -9,7 +9,9 @@ package io.element.android.tests.testutils import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import io.element.android.libraries.designsystem.utils.LocalUiTestMode import org.junit.Assert.assertFalse import org.junit.rules.TestRule import kotlin.coroutines.CoroutineContext @@ -49,7 +51,16 @@ object RobolectricDispatcherCleaner { } } -fun AndroidComposeTestRule.setSafeContent(content: @Composable () -> Unit) { - RobolectricDispatcherCleaner.clearAndroidUiDispatcher() - setContent(content) +fun AndroidComposeTestRule.setSafeContent( + clearAndroidUiDispatcher: Boolean = false, + content: @Composable () -> Unit, +) { + if (clearAndroidUiDispatcher) { + RobolectricDispatcherCleaner.clearAndroidUiDispatcher() + } + setContent { + CompositionLocalProvider(LocalUiTestMode provides true) { + content() + } + } } From 1795371d90afee6290696d84e154dbb67c97920a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 3 Jun 2025 18:25:38 +0200 Subject: [PATCH 11/12] Fix broken test --- .../screens/loginpassword/LoginPasswordViewTest.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt index 45d7bc53e5..99deb8b7bb 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt @@ -8,17 +8,21 @@ package io.element.android.features.login.impl.screens.loginpassword import androidx.activity.ComponentActivity +import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.matrix.test.A_PASSWORD import io.element.android.libraries.matrix.test.A_USER_NAME +import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EventsRecorder @@ -120,15 +124,15 @@ class LoginPasswordViewTest { eventSink = eventsRecorder, ), ) - rule.onNodeWithText(A_PASSWORD).assertDoesNotExist() + rule.onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••")) // Show password val a11yShowPassword = rule.activity.getString(CommonStrings.a11y_show_password) rule.onNodeWithContentDescription(a11yShowPassword).performClick() - rule.onNodeWithText(A_PASSWORD).assertExists() + rule.onNodeWithTag(TestTags.loginPassword.value).assert(hasText(A_PASSWORD)) // Hide password val a11yHidePassword = rule.activity.getString(CommonStrings.a11y_hide_password) rule.onNodeWithContentDescription(a11yHidePassword).performClick() - rule.onNodeWithText(A_PASSWORD).assertDoesNotExist() + rule.onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••")) } @Test From f3aec47b58e8b299d86dd3f76a64e41273c9b25a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 3 Jun 2025 21:57:46 +0200 Subject: [PATCH 12/12] Apply fix to RoomMemberModerationViewTest and RoomListDeclineInviteMenuTest --- .../roomlist/impl/RoomListDeclineInviteMenuTest.kt | 9 +++++---- .../impl/RoomMemberModerationViewTest.kt | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListDeclineInviteMenuTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListDeclineInviteMenuTest.kt index 0fdb5d7639..8c75027ec5 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListDeclineInviteMenuTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListDeclineInviteMenuTest.kt @@ -16,6 +16,7 @@ import io.element.android.tests.testutils.EnsureCalledOnceWithParam import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.setSafeContent import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -28,7 +29,7 @@ class RoomListDeclineInviteMenuTest { fun `clicking on decline emits the expected Events`() { val eventsRecorder = EventsRecorder() val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) - rule.setContent { + rule.setSafeContent { RoomListDeclineInviteMenu( menu = menu, canReportRoom = false, @@ -49,7 +50,7 @@ class RoomListDeclineInviteMenuTest { fun `clicking on decline and block when canReportRoom=true, it emits the expected Events and callback`() { val eventsRecorder = EventsRecorder() val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) - rule.setContent { + rule.setSafeContent { RoomListDeclineInviteMenu( menu = menu, canReportRoom = true, @@ -66,7 +67,7 @@ class RoomListDeclineInviteMenuTest { fun `clicking on decline and block when canReportRoom=false, it emits the expected Events`() { val eventsRecorder = EventsRecorder() val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) - rule.setContent { + rule.setSafeContent { RoomListDeclineInviteMenu( menu = menu, canReportRoom = false, @@ -86,7 +87,7 @@ class RoomListDeclineInviteMenuTest { fun `clicking on cancel emits the expected Event`() { val eventsRecorder = EventsRecorder() val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) - rule.setContent { + rule.setSafeContent { RoomListDeclineInviteMenu( menu = menu, canReportRoom = false, diff --git a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt index d17b7579b6..fc1b815562 100644 --- a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt +++ b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt @@ -22,6 +22,7 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnceWithTwoParams import io.element.android.tests.testutils.pressTag +import io.element.android.tests.testutils.setSafeContent import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule @@ -217,7 +218,7 @@ private fun AndroidComposeTestRule.setRoomM state: InternalRoomMemberModerationState, onSelectAction: (ModerationAction, MatrixUser) -> Unit = EnsureNeverCalledWithTwoParams(), ) { - setContent { + setSafeContent { RoomMemberModerationView( state = state, onSelectAction = onSelectAction,