Few initial motion tests for both, STL and Flexiglass
Bug: 322324387
Test: Unit tests
Flag: TEST_ONLY
Change-Id: Ic532678b268e16b8300db189d7d15b9580dc9102
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index d2ca112..e4a4159 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -147,6 +147,9 @@
"tests/src/**/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt",
"tests/src/**/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt",
"tests/src/**/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt",
+ // TODO(b/322324387): Fails to start due to missing ScreenshotActivity
+ "tests/src/**/systemui/bouncer/ui/composable/BouncerContentTest.kt",
+ "tests/src/**/systemui/bouncer/ui/composable/PatternBouncerTest.kt",
"tests/src/**/systemui/broadcast/UserBroadcastDispatcherTest.kt",
"tests/src/**/systemui/charging/WiredChargingRippleControllerTest.kt",
"tests/src/**/systemui/clipboardoverlay/ClipboardModelTest.kt",
@@ -487,6 +490,8 @@
"SystemUI-res",
"WifiTrackerLib",
"PlatformAnimationLib",
+ "PlatformMotionTestingCompose",
+ "ScreenshotComposeUtilsLib",
"SystemUIPluginLib",
"SystemUISharedLib",
"SystemUICustomizationLib",
diff --git a/packages/SystemUI/compose/core/Android.bp b/packages/SystemUI/compose/core/Android.bp
index 49ae821..c63c2b4 100644
--- a/packages/SystemUI/compose/core/Android.bp
+++ b/packages/SystemUI/compose/core/Android.bp
@@ -32,6 +32,7 @@
static_libs: [
"PlatformAnimationLib",
+ "PlatformMotionTestingComposeValues",
"androidx.compose.runtime_runtime",
"androidx.compose.material3_material3",
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
index fa01a4b..c19c08e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
@@ -20,6 +20,7 @@
import android.app.AlertDialog
import android.content.DialogInterface
+import androidx.annotation.VisibleForTesting
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.animateFloatAsState
@@ -69,6 +70,7 @@
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
@@ -104,6 +106,9 @@
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.pow
+import platform.test.motion.compose.values.MotionTestValueKey
+import platform.test.motion.compose.values.MotionTestValues
+import platform.test.motion.compose.values.motionTestValues
@Composable
fun BouncerContent(
@@ -114,6 +119,17 @@
val isSideBySideSupported by viewModel.isSideBySideSupported.collectAsStateWithLifecycle()
val layout = calculateLayout(isSideBySideSupported = isSideBySideSupported)
+ BouncerContent(layout, viewModel, dialogFactory, modifier)
+}
+
+@Composable
+@VisibleForTesting
+fun BouncerContent(
+ layout: BouncerSceneLayout,
+ viewModel: BouncerViewModel,
+ dialogFactory: BouncerDialogFactory,
+ modifier: Modifier
+) {
Box(
// Allows the content within each of the layouts to react to the appearance and
// disappearance of the IME, which is also known as the software keyboard.
@@ -318,6 +334,8 @@
LocalWindowSizeClass.current.heightSizeClass == WindowHeightSizeClass.Expanded
val authMethod by viewModel.authMethodViewModel.collectAsStateWithLifecycle()
+ var swapAnimationEnd by remember { mutableStateOf(false) }
+
Row(
modifier =
modifier
@@ -331,11 +349,16 @@
}
)
}
+ .testTag("BesideUserSwitcherLayout")
+ .motionTestValues {
+ swapAnimationEnd exportAs BouncerMotionTestKeys.swapAnimationEnd
+ }
.padding(
top = if (isHeightExpanded) 128.dp else 96.dp,
bottom = if (isHeightExpanded) 128.dp else 48.dp,
),
) {
+ LaunchedEffect(isSwapped) { swapAnimationEnd = false }
val animatedOffset by
animateFloatAsState(
targetValue =
@@ -354,31 +377,35 @@
-1f
},
label = "offset",
- )
+ ) {
+ swapAnimationEnd = true
+ }
fun Modifier.swappable(inversed: Boolean = false): Modifier {
return graphicsLayer {
- translationX =
- size.width *
- animatedOffset *
- if (inversed) {
- // A negative sign is used to make sure this is offset in the direction
- // that's opposite to the direction that the user switcher is pushed in.
- -1
- } else {
- 1
- }
- alpha = animatedAlpha(animatedOffset)
- }
+ translationX =
+ size.width *
+ animatedOffset *
+ if (inversed) {
+ // A negative sign is used to make sure this is offset in the
+ // direction that's opposite to the direction that the user
+ // switcher is pushed in.
+ -1
+ } else {
+ 1
+ }
+ alpha = animatedAlpha(animatedOffset)
+ }
+ .motionTestValues { animatedAlpha(animatedOffset) exportAs MotionTestValues.alpha }
}
UserSwitcher(
viewModel = viewModel,
- modifier = Modifier.weight(1f).swappable(),
+ modifier = Modifier.weight(1f).swappable().testTag("UserSwitcher"),
)
FoldAware(
- modifier = Modifier.weight(1f).swappable(inversed = true),
+ modifier = Modifier.weight(1f).swappable(inversed = true).testTag("FoldAware"),
viewModel = viewModel,
aboveFold = {
Column(
@@ -389,7 +416,10 @@
viewModel = viewModel.message,
)
- OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp))
+ OutputArea(
+ viewModel = viewModel,
+ modifier = Modifier.padding(top = 24.dp).testTag("OutputArea")
+ )
}
},
belowFold = {
@@ -412,13 +442,13 @@
viewModel = viewModel,
pinButtonRowVerticalSpacing = 12.dp,
centerPatternDotsVertically = true,
- modifier = Modifier.align(Alignment.BottomCenter),
+ modifier = Modifier.align(Alignment.BottomCenter).testTag("InputArea"),
)
}
ActionArea(
viewModel = viewModel,
- modifier = Modifier.padding(top = 48.dp),
+ modifier = Modifier.padding(top = 48.dp).testTag("ActionArea"),
)
}
},
@@ -938,3 +968,8 @@
private val SceneTransitions = transitions {
from(SceneKeys.ContiguousSceneKey, to = SceneKeys.SplitSceneKey) { spec = tween() }
}
+
+@VisibleForTesting
+object BouncerMotionTestKeys {
+ val swapAnimationEnd = MotionTestValueKey<Boolean>("swapAnimationEnd")
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
index 9c2fd64..069113b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
@@ -17,6 +17,7 @@
package com.android.systemui.bouncer.ui.composable
import android.view.HapticFeedbackConstants
+import androidx.annotation.VisibleForTesting
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.tween
@@ -50,6 +51,10 @@
import com.android.compose.animation.Easings
import com.android.compose.modifiers.thenIf
import com.android.internal.R
+import com.android.systemui.bouncer.ui.composable.MotionTestKeys.dotAppearFadeIn
+import com.android.systemui.bouncer.ui.composable.MotionTestKeys.dotAppearMoveUp
+import com.android.systemui.bouncer.ui.composable.MotionTestKeys.dotScaling
+import com.android.systemui.bouncer.ui.composable.MotionTestKeys.entryCompleted
import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel
import com.android.systemui.compose.modifiers.sysuiResTag
@@ -58,6 +63,8 @@
import kotlin.math.sqrt
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
+import platform.test.motion.compose.values.MotionTestValueKey
+import platform.test.motion.compose.values.motionTestValues
/**
* UI for the input part of a pattern-requiring version of the bouncer.
@@ -68,7 +75,8 @@
* `false`, the dots will be pushed towards the end/bottom of the axis.
*/
@Composable
-internal fun PatternBouncer(
+@VisibleForTesting
+fun PatternBouncer(
viewModel: PatternBouncerViewModel,
centerDotsVertically: Boolean,
modifier: Modifier = Modifier,
@@ -111,33 +119,11 @@
remember(dots) {
dots.associateWith { dot -> with(density) { (80 + (20 * dot.y)).dp.toPx() } }
}
+
+ var entryAnimationCompleted by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
- dotAppearFadeInAnimatables.forEach { (dot, animatable) ->
- scope.launch {
- animatable.animateTo(
- targetValue = 1f,
- animationSpec =
- tween(
- delayMillis = 33 * dot.y,
- durationMillis = 450,
- easing = Easings.LegacyDecelerate,
- )
- )
- }
- }
- dotAppearMoveUpAnimatables.forEach { (dot, animatable) ->
- scope.launch {
- animatable.animateTo(
- targetValue = 1f,
- animationSpec =
- tween(
- delayMillis = 0,
- durationMillis = 450 + (33 * dot.y),
- easing = Easings.StandardDecelerate,
- )
- )
- }
- }
+ showEntryAnimation(dotAppearFadeInAnimatables, dotAppearMoveUpAnimatables)
+ entryAnimationCompleted = true
}
val view = LocalView.current
@@ -286,6 +272,12 @@
}
}
}
+ .motionTestValues {
+ entryAnimationCompleted exportAs entryCompleted
+ dotAppearFadeInAnimatables.map { it.value.value } exportAs dotAppearFadeIn
+ dotAppearMoveUpAnimatables.map { it.value.value } exportAs dotAppearMoveUp
+ dotScalingAnimatables.map { it.value.value } exportAs dotScaling
+ }
) {
gridCoordinates?.let { nonNullCoordinates ->
val containerSize = nonNullCoordinates.size
@@ -381,6 +373,40 @@
}
}
+private suspend fun showEntryAnimation(
+ dotAppearFadeInAnimatables: Map<PatternDotViewModel, Animatable<Float, AnimationVector1D>>,
+ dotAppearMoveUpAnimatables: Map<PatternDotViewModel, Animatable<Float, AnimationVector1D>>,
+) {
+ coroutineScope {
+ dotAppearFadeInAnimatables.forEach { (dot, animatable) ->
+ launch {
+ animatable.animateTo(
+ targetValue = 1f,
+ animationSpec =
+ tween(
+ delayMillis = 33 * dot.y,
+ durationMillis = 450,
+ easing = Easings.LegacyDecelerate,
+ )
+ )
+ }
+ }
+ dotAppearMoveUpAnimatables.forEach { (dot, animatable) ->
+ launch {
+ animatable.animateTo(
+ targetValue = 1f,
+ animationSpec =
+ tween(
+ delayMillis = 0,
+ durationMillis = 450 + (33 * dot.y),
+ easing = Easings.StandardDecelerate,
+ )
+ )
+ }
+ }
+ }
+}
+
/** Returns an [Offset] representation of the given [dot], in pixel coordinates. */
private fun pixelOffset(
dot: PatternDotViewModel,
@@ -490,3 +516,11 @@
private const val FAILURE_ANIMATION_DOT_SHRINK_ANIMATION_DURATION_MS = 50
private const val FAILURE_ANIMATION_DOT_SHRINK_STAGGER_DELAY_MS = 33
private const val FAILURE_ANIMATION_DOT_REVERT_ANIMATION_DURATION = 617
+
+@VisibleForTesting
+object MotionTestKeys {
+ val entryCompleted = MotionTestValueKey<Boolean>("PinBouncer::entryAnimationCompleted")
+ val dotAppearFadeIn = MotionTestValueKey<List<Float>>("PinBouncer::dotAppearFadeIn")
+ val dotAppearMoveUp = MotionTestValueKey<List<Float>>("PinBouncer::dotAppearMoveUp")
+ val dotScaling = MotionTestValueKey<List<Float>>("PinBouncer::dotScaling")
+}
diff --git a/packages/SystemUI/compose/scene/tests/Android.bp b/packages/SystemUI/compose/scene/tests/Android.bp
index af13896..3509504 100644
--- a/packages/SystemUI/compose/scene/tests/Android.bp
+++ b/packages/SystemUI/compose/scene/tests/Android.bp
@@ -25,8 +25,8 @@
android_test {
name: "PlatformComposeSceneTransitionLayoutTests",
manifest: "AndroidManifest.xml",
+ defaults: ["MotionTestDefaults"],
test_suites: ["device-tests"],
- certificate: "platform",
srcs: [
"src/**/*.kt",
@@ -38,7 +38,7 @@
static_libs: [
"PlatformComposeSceneTransitionLayoutTestsUtils",
-
+ "PlatformMotionTestingCompose",
"androidx.test.runner",
"androidx.test.ext.junit",
@@ -48,7 +48,7 @@
"truth",
],
-
+ asset_dirs: ["goldens"],
kotlincflags: ["-Xjvm-default=all"],
use_resource_processor: true,
}
diff --git a/packages/SystemUI/compose/scene/tests/AndroidManifest.xml b/packages/SystemUI/compose/scene/tests/AndroidManifest.xml
index 1a9172e..174ad30 100644
--- a/packages/SystemUI/compose/scene/tests/AndroidManifest.xml
+++ b/packages/SystemUI/compose/scene/tests/AndroidManifest.xml
@@ -17,6 +17,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.compose.animation.scene.tests" >
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<application>
<uses-library android:name="android.test.runner" />
</application>
diff --git a/packages/SystemUI/compose/scene/tests/goldens/testAnchoredHeightOnly.json b/packages/SystemUI/compose/scene/tests/goldens/testAnchoredHeightOnly.json
new file mode 100644
index 0000000..0843663
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/goldens/testAnchoredHeightOnly.json
@@ -0,0 +1,41 @@
+{
+ "frame_ids": [
+ "before",
+ 0,
+ 16,
+ 32,
+ 48,
+ "after"
+ ],
+ "features": [
+ {
+ "name": "Bar_size",
+ "type": "dpSize",
+ "data_points": [
+ {
+ "type": "not_found"
+ },
+ {
+ "width": 200,
+ "height": 100
+ },
+ {
+ "width": 200,
+ "height": 90
+ },
+ {
+ "width": 200,
+ "height": 80
+ },
+ {
+ "width": 200,
+ "height": 70
+ },
+ {
+ "width": 200,
+ "height": 60
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/compose/scene/tests/goldens/testAnchoredSizeEnter.json b/packages/SystemUI/compose/scene/tests/goldens/testAnchoredSizeEnter.json
new file mode 100644
index 0000000..2df44091
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/goldens/testAnchoredSizeEnter.json
@@ -0,0 +1,41 @@
+{
+ "frame_ids": [
+ "before",
+ 0,
+ 16,
+ 32,
+ 48,
+ "after"
+ ],
+ "features": [
+ {
+ "name": "Bar_size",
+ "type": "dpSize",
+ "data_points": [
+ {
+ "type": "not_found"
+ },
+ {
+ "width": 100,
+ "height": 100
+ },
+ {
+ "width": 125.14286,
+ "height": 90
+ },
+ {
+ "width": 150,
+ "height": 80
+ },
+ {
+ "width": 175.14285,
+ "height": 70
+ },
+ {
+ "width": 200,
+ "height": 60
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/compose/scene/tests/goldens/testAnchoredSizeExit.json b/packages/SystemUI/compose/scene/tests/goldens/testAnchoredSizeExit.json
new file mode 100644
index 0000000..2b0a954
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/goldens/testAnchoredSizeExit.json
@@ -0,0 +1,41 @@
+{
+ "frame_ids": [
+ "before",
+ 0,
+ 16,
+ 32,
+ 48,
+ "after"
+ ],
+ "features": [
+ {
+ "name": "Bar_size",
+ "type": "dpSize",
+ "data_points": [
+ {
+ "width": 100,
+ "height": 100
+ },
+ {
+ "width": 100,
+ "height": 100
+ },
+ {
+ "width": 125.14286,
+ "height": 90
+ },
+ {
+ "width": 150,
+ "height": 80
+ },
+ {
+ "width": 175.14285,
+ "height": 70
+ },
+ {
+ "type": "not_found"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/compose/scene/tests/goldens/testAnchoredWidthOnly.json b/packages/SystemUI/compose/scene/tests/goldens/testAnchoredWidthOnly.json
new file mode 100644
index 0000000..027df29
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/goldens/testAnchoredWidthOnly.json
@@ -0,0 +1,41 @@
+{
+ "frame_ids": [
+ "before",
+ 0,
+ 16,
+ 32,
+ 48,
+ "after"
+ ],
+ "features": [
+ {
+ "name": "Bar_size",
+ "type": "dpSize",
+ "data_points": [
+ {
+ "type": "not_found"
+ },
+ {
+ "width": 100,
+ "height": 60
+ },
+ {
+ "width": 125.14286,
+ "height": 60
+ },
+ {
+ "width": 150,
+ "height": 60
+ },
+ {
+ "width": 175.14285,
+ "height": 60
+ },
+ {
+ "width": 200,
+ "height": 60
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt
index e555a01..7b99212 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt
@@ -20,50 +20,48 @@
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.TestElements
-import com.android.compose.animation.scene.testTransition
-import com.android.compose.test.assertSizeIsEqualTo
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.compose.animation.scene.TransitionRecordingSpec
+import com.android.compose.animation.scene.featureOfElement
+import com.android.compose.animation.scene.recordTransition
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import platform.test.motion.compose.ComposeFeatureCaptures
+import platform.test.motion.compose.createComposeMotionTestRule
+import platform.test.motion.testing.createGoldenPathManager
@RunWith(AndroidJUnit4::class)
class AnchoredSizeTest {
- @get:Rule val rule = createComposeRule()
+ private val goldenPaths =
+ createGoldenPathManager("frameworks/base/packages/SystemUI/compose/scene/tests/goldens")
+
+ @get:Rule val motionRule = createComposeMotionTestRule(goldenPaths)
@Test
fun testAnchoredSizeEnter() {
- rule.testTransition(
+ assertBarSizeMatchesGolden(
fromSceneContent = { Box(Modifier.size(100.dp, 100.dp).element(TestElements.Foo)) },
toSceneContent = {
Box(Modifier.size(50.dp, 50.dp).element(TestElements.Foo))
Box(Modifier.size(200.dp, 60.dp).element(TestElements.Bar))
},
transition = {
- // Scale during 4 frames.
spec = tween(16 * 4, easing = LinearEasing)
anchoredSize(TestElements.Bar, TestElements.Foo)
- },
- ) {
- // Bar is entering. It starts at the same size as Foo in scene A in and scales to its
- // final size in scene B.
- before { onElement(TestElements.Bar).assertDoesNotExist() }
- at(0) { onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp) }
- at(16) { onElement(TestElements.Bar).assertSizeIsEqualTo(125.dp, 90.dp) }
- at(32) { onElement(TestElements.Bar).assertSizeIsEqualTo(150.dp, 80.dp) }
- at(48) { onElement(TestElements.Bar).assertSizeIsEqualTo(175.dp, 70.dp) }
- at(64) { onElement(TestElements.Bar).assertSizeIsEqualTo(200.dp, 60.dp) }
- after { onElement(TestElements.Bar).assertSizeIsEqualTo(200.dp, 60.dp) }
- }
+ }
+ )
}
@Test
fun testAnchoredSizeExit() {
- rule.testTransition(
+ assertBarSizeMatchesGolden(
fromSceneContent = {
Box(Modifier.size(100.dp, 100.dp).element(TestElements.Foo))
Box(Modifier.size(100.dp, 100.dp).element(TestElements.Bar))
@@ -73,22 +71,13 @@
// Scale during 4 frames.
spec = tween(16 * 4, easing = LinearEasing)
anchoredSize(TestElements.Bar, TestElements.Foo)
- },
- ) {
- // Bar is leaving. It starts at 100dp x 100dp in scene A and is scaled to 200dp x 60dp,
- // the size of Foo in scene B.
- before { onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp) }
- at(0) { onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp) }
- at(16) { onElement(TestElements.Bar).assertSizeIsEqualTo(125.dp, 90.dp) }
- at(32) { onElement(TestElements.Bar).assertSizeIsEqualTo(150.dp, 80.dp) }
- at(48) { onElement(TestElements.Bar).assertSizeIsEqualTo(175.dp, 70.dp) }
- after { onElement(TestElements.Bar).assertDoesNotExist() }
- }
+ }
+ )
}
@Test
fun testAnchoredWidthOnly() {
- rule.testTransition(
+ assertBarSizeMatchesGolden(
fromSceneContent = { Box(Modifier.size(100.dp, 100.dp).element(TestElements.Foo)) },
toSceneContent = {
Box(Modifier.size(50.dp, 50.dp).element(TestElements.Foo))
@@ -98,20 +87,12 @@
spec = tween(16 * 4, easing = LinearEasing)
anchoredSize(TestElements.Bar, TestElements.Foo, anchorHeight = false)
},
- ) {
- before { onElement(TestElements.Bar).assertDoesNotExist() }
- at(0) { onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 60.dp) }
- at(16) { onElement(TestElements.Bar).assertSizeIsEqualTo(125.dp, 60.dp) }
- at(32) { onElement(TestElements.Bar).assertSizeIsEqualTo(150.dp, 60.dp) }
- at(48) { onElement(TestElements.Bar).assertSizeIsEqualTo(175.dp, 60.dp) }
- at(64) { onElement(TestElements.Bar).assertSizeIsEqualTo(200.dp, 60.dp) }
- after { onElement(TestElements.Bar).assertSizeIsEqualTo(200.dp, 60.dp) }
- }
+ )
}
@Test
fun testAnchoredHeightOnly() {
- rule.testTransition(
+ assertBarSizeMatchesGolden(
fromSceneContent = { Box(Modifier.size(100.dp, 100.dp).element(TestElements.Foo)) },
toSceneContent = {
Box(Modifier.size(50.dp, 50.dp).element(TestElements.Foo))
@@ -120,15 +101,23 @@
transition = {
spec = tween(16 * 4, easing = LinearEasing)
anchoredSize(TestElements.Bar, TestElements.Foo, anchorWidth = false)
- },
- ) {
- before { onElement(TestElements.Bar).assertDoesNotExist() }
- at(0) { onElement(TestElements.Bar).assertSizeIsEqualTo(200.dp, 100.dp) }
- at(16) { onElement(TestElements.Bar).assertSizeIsEqualTo(200.dp, 90.dp) }
- at(32) { onElement(TestElements.Bar).assertSizeIsEqualTo(200.dp, 80.dp) }
- at(48) { onElement(TestElements.Bar).assertSizeIsEqualTo(200.dp, 70.dp) }
- at(64) { onElement(TestElements.Bar).assertSizeIsEqualTo(200.dp, 60.dp) }
- after { onElement(TestElements.Bar).assertSizeIsEqualTo(200.dp, 60.dp) }
- }
+ }
+ )
+ }
+
+ private fun assertBarSizeMatchesGolden(
+ fromSceneContent: @Composable SceneScope.() -> Unit,
+ toSceneContent: @Composable SceneScope.() -> Unit,
+ transition: TransitionBuilder.() -> Unit,
+ ) {
+ val recordingSpec =
+ TransitionRecordingSpec(recordAfter = true) {
+ featureOfElement(TestElements.Bar, ComposeFeatureCaptures.dpSize)
+ }
+
+ val motion =
+ motionRule.recordTransition(fromSceneContent, toSceneContent, transition, recordingSpec)
+
+ motionRule.assertThat(motion).timeSeriesMatchesGolden()
}
}
diff --git a/packages/SystemUI/compose/scene/tests/utils/Android.bp b/packages/SystemUI/compose/scene/tests/utils/Android.bp
index 9089e6a..292efa0 100644
--- a/packages/SystemUI/compose/scene/tests/utils/Android.bp
+++ b/packages/SystemUI/compose/scene/tests/utils/Android.bp
@@ -32,6 +32,7 @@
static_libs: [
"PlatformComposeSceneTransitionLayout",
+ "PlatformMotionTestingCompose",
"androidx.compose.runtime_runtime",
"androidx.compose.ui_ui-test-junit4",
],
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
index 2d71a6e..4225291 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
@@ -17,12 +17,24 @@
package com.android.compose.animation.scene
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import platform.test.motion.MotionTestRule
+import platform.test.motion.RecordedMotion
+import platform.test.motion.compose.ComposeRecordingSpec
+import platform.test.motion.compose.ComposeToolkit
+import platform.test.motion.compose.MotionControl
+import platform.test.motion.compose.feature
+import platform.test.motion.compose.recordMotion
+import platform.test.motion.golden.FeatureCapture
+import platform.test.motion.golden.TimeSeriesCaptureScope
@DslMarker annotation class TransitionTestDsl
@@ -100,6 +112,63 @@
)
}
+data class TransitionRecordingSpec(
+ val recordBefore: Boolean = true,
+ val recordAfter: Boolean = true,
+ val timeSeriesCapture: TimeSeriesCaptureScope<SemanticsNodeInteractionsProvider>.() -> Unit
+)
+
+/** Captures the feature using [capture] on the [element]. */
+fun TimeSeriesCaptureScope<SemanticsNodeInteractionsProvider>.featureOfElement(
+ element: ElementKey,
+ capture: FeatureCapture<SemanticsNode, *>,
+ name: String = "${element.debugName}_${capture.name}"
+) {
+ feature(isElement(element), capture, name)
+}
+
+/** Records the transition between two scenes of [transitionLayout][SceneTransitionLayout]. */
+fun MotionTestRule<ComposeToolkit>.recordTransition(
+ fromSceneContent: @Composable SceneScope.() -> Unit,
+ toSceneContent: @Composable SceneScope.() -> Unit,
+ transition: TransitionBuilder.() -> Unit,
+ recordingSpec: TransitionRecordingSpec,
+ layoutModifier: Modifier = Modifier,
+ fromScene: SceneKey = TestScenes.SceneA,
+ toScene: SceneKey = TestScenes.SceneB,
+): RecordedMotion {
+ val state =
+ MutableSceneTransitionLayoutState(
+ fromScene,
+ transitions { from(fromScene, to = toScene, builder = transition) }
+ )
+ return recordMotion(
+ content = { play ->
+ LaunchedEffect(play) {
+ if (play) {
+ state.setTargetScene(toScene, coroutineScope = this)
+ }
+ }
+
+ SceneTransitionLayout(
+ state,
+ layoutModifier,
+ ) {
+ scene(fromScene, content = fromSceneContent)
+ scene(toScene, content = toSceneContent)
+ }
+ },
+ ComposeRecordingSpec(
+ MotionControl(delayRecording = { awaitCondition { state.isTransitioning() } }) {
+ awaitCondition { !state.isTransitioning() }
+ },
+ recordBefore = recordingSpec.recordBefore,
+ recordAfter = recordingSpec.recordAfter,
+ timeSeriesCapture = recordingSpec.timeSeriesCapture
+ )
+ )
+}
+
/**
* Test the transition between two scenes of [transitionLayout][SceneTransitionLayout] at different
* points in time.
diff --git a/packages/SystemUI/tests/goldens/animateFailure.json b/packages/SystemUI/tests/goldens/animateFailure.json
new file mode 100644
index 0000000..a008f92
--- /dev/null
+++ b/packages/SystemUI/tests/goldens/animateFailure.json
@@ -0,0 +1,612 @@
+{
+ "frame_ids": [
+ "before",
+ 0,
+ 16,
+ 32,
+ 48,
+ 64,
+ 80,
+ 96,
+ 112,
+ 128,
+ 144,
+ 160,
+ 176,
+ 192,
+ 208,
+ 224,
+ 240,
+ 256,
+ 272,
+ 288,
+ 304,
+ 320,
+ 336,
+ 352,
+ 368,
+ 384,
+ 400,
+ 416,
+ 432,
+ 448,
+ 464,
+ 480,
+ 496,
+ 512,
+ 528,
+ 544,
+ 560,
+ 576,
+ 592,
+ 608,
+ 624,
+ 640,
+ 656,
+ 672,
+ 688,
+ 704,
+ 720,
+ 736,
+ 752,
+ "after"
+ ],
+ "features": [
+ {
+ "name": "PinBouncer::dotScaling",
+ "type": "float[]",
+ "data_points": [
+ [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ [
+ 0.93142855,
+ 1,
+ 1,
+ 0.93142855,
+ 1,
+ 1,
+ 0.93142855,
+ 1,
+ 1
+ ],
+ [
+ 0.86285716,
+ 1,
+ 1,
+ 0.86285716,
+ 1,
+ 1,
+ 0.86285716,
+ 1,
+ 1
+ ],
+ [
+ 0.7942857,
+ 0.93571424,
+ 1,
+ 0.7942857,
+ 0.93571424,
+ 1,
+ 0.7942857,
+ 0.93571424,
+ 1
+ ],
+ [
+ 0.78571427,
+ 0.86714286,
+ 1,
+ 0.78571427,
+ 0.86714286,
+ 1,
+ 0.78571427,
+ 0.86714286,
+ 1
+ ],
+ [
+ 0.78571427,
+ 0.7985714,
+ 0.94000006,
+ 0.78571427,
+ 0.7985714,
+ 0.94000006,
+ 0.78571427,
+ 0.7985714,
+ 0.94000006
+ ],
+ [
+ 0.7872844,
+ 0.78571427,
+ 0.87142855,
+ 0.7872844,
+ 0.78571427,
+ 0.87142855,
+ 0.7872844,
+ 0.78571427,
+ 0.87142855
+ ],
+ [
+ 0.7925441,
+ 0.78571427,
+ 0.8028571,
+ 0.7925441,
+ 0.78571427,
+ 0.8028571,
+ 0.7925441,
+ 0.78571427,
+ 0.8028571
+ ],
+ [
+ 0.8037901,
+ 0.7872844,
+ 0.78571427,
+ 0.8037901,
+ 0.7872844,
+ 0.78571427,
+ 0.8037901,
+ 0.7872844,
+ 0.78571427
+ ],
+ [
+ 0.8223549,
+ 0.7925441,
+ 0.78571427,
+ 0.8223549,
+ 0.7925441,
+ 0.78571427,
+ 0.8223549,
+ 0.7925441,
+ 0.78571427
+ ],
+ [
+ 0.84427696,
+ 0.8037901,
+ 0.7872844,
+ 0.84427696,
+ 0.8037901,
+ 0.7872844,
+ 0.84427696,
+ 0.8037901,
+ 0.7872844
+ ],
+ [
+ 0.864585,
+ 0.8223549,
+ 0.7925441,
+ 0.864585,
+ 0.8223549,
+ 0.7925441,
+ 0.864585,
+ 0.8223549,
+ 0.7925441
+ ],
+ [
+ 0.8817363,
+ 0.84427696,
+ 0.8037901,
+ 0.8817363,
+ 0.84427696,
+ 0.8037901,
+ 0.8817363,
+ 0.84427696,
+ 0.8037901
+ ],
+ [
+ 0.89635646,
+ 0.864585,
+ 0.8223549,
+ 0.89635646,
+ 0.864585,
+ 0.8223549,
+ 0.89635646,
+ 0.864585,
+ 0.8223549
+ ],
+ [
+ 0.908528,
+ 0.8817363,
+ 0.84427696,
+ 0.908528,
+ 0.8817363,
+ 0.84427696,
+ 0.908528,
+ 0.8817363,
+ 0.84427696
+ ],
+ [
+ 0.91881,
+ 0.89635646,
+ 0.864585,
+ 0.91881,
+ 0.89635646,
+ 0.864585,
+ 0.91881,
+ 0.89635646,
+ 0.864585
+ ],
+ [
+ 0.9280548,
+ 0.908528,
+ 0.8817363,
+ 0.9280548,
+ 0.908528,
+ 0.8817363,
+ 0.9280548,
+ 0.908528,
+ 0.8817363
+ ],
+ [
+ 0.9362979,
+ 0.91881,
+ 0.89635646,
+ 0.9362979,
+ 0.91881,
+ 0.89635646,
+ 0.9362979,
+ 0.91881,
+ 0.89635646
+ ],
+ [
+ 0.9433999,
+ 0.9280548,
+ 0.908528,
+ 0.9433999,
+ 0.9280548,
+ 0.908528,
+ 0.9433999,
+ 0.9280548,
+ 0.908528
+ ],
+ [
+ 0.9497632,
+ 0.9362979,
+ 0.91881,
+ 0.9497632,
+ 0.9362979,
+ 0.91881,
+ 0.9497632,
+ 0.9362979,
+ 0.91881
+ ],
+ [
+ 0.9552536,
+ 0.9433999,
+ 0.9280548,
+ 0.9552536,
+ 0.9433999,
+ 0.9280548,
+ 0.9552536,
+ 0.9433999,
+ 0.9280548
+ ],
+ [
+ 0.96035147,
+ 0.9497632,
+ 0.9362979,
+ 0.96035147,
+ 0.9497632,
+ 0.9362979,
+ 0.96035147,
+ 0.9497632,
+ 0.9362979
+ ],
+ [
+ 0.9649086,
+ 0.9552536,
+ 0.9433999,
+ 0.9649086,
+ 0.9552536,
+ 0.9433999,
+ 0.9649086,
+ 0.9552536,
+ 0.9433999
+ ],
+ [
+ 0.96897155,
+ 0.96035147,
+ 0.9497632,
+ 0.96897155,
+ 0.96035147,
+ 0.9497632,
+ 0.96897155,
+ 0.96035147,
+ 0.9497632
+ ],
+ [
+ 0.9727647,
+ 0.9649086,
+ 0.9552536,
+ 0.9727647,
+ 0.9649086,
+ 0.9552536,
+ 0.9727647,
+ 0.9649086,
+ 0.9552536
+ ],
+ [
+ 0.9760455,
+ 0.96897155,
+ 0.96035147,
+ 0.9760455,
+ 0.96897155,
+ 0.96035147,
+ 0.9760455,
+ 0.96897155,
+ 0.96035147
+ ],
+ [
+ 0.97915274,
+ 0.9727647,
+ 0.9649086,
+ 0.97915274,
+ 0.9727647,
+ 0.9649086,
+ 0.97915274,
+ 0.9727647,
+ 0.9649086
+ ],
+ [
+ 0.98185575,
+ 0.9760455,
+ 0.96897155,
+ 0.98185575,
+ 0.9760455,
+ 0.96897155,
+ 0.98185575,
+ 0.9760455,
+ 0.96897155
+ ],
+ [
+ 0.98434585,
+ 0.97915274,
+ 0.9727647,
+ 0.98434585,
+ 0.97915274,
+ 0.9727647,
+ 0.98434585,
+ 0.97915274,
+ 0.9727647
+ ],
+ [
+ 0.9866356,
+ 0.98185575,
+ 0.9760455,
+ 0.9866356,
+ 0.98185575,
+ 0.9760455,
+ 0.9866356,
+ 0.98185575,
+ 0.9760455
+ ],
+ [
+ 0.98856884,
+ 0.98434585,
+ 0.97915274,
+ 0.98856884,
+ 0.98434585,
+ 0.97915274,
+ 0.98856884,
+ 0.98434585,
+ 0.97915274
+ ],
+ [
+ 0.99050206,
+ 0.9866356,
+ 0.98185575,
+ 0.99050206,
+ 0.9866356,
+ 0.98185575,
+ 0.99050206,
+ 0.9866356,
+ 0.98185575
+ ],
+ [
+ 0.9920071,
+ 0.98856884,
+ 0.98434585,
+ 0.9920071,
+ 0.98856884,
+ 0.98434585,
+ 0.9920071,
+ 0.98856884,
+ 0.98434585
+ ],
+ [
+ 0.99343646,
+ 0.99050206,
+ 0.9866356,
+ 0.99343646,
+ 0.99050206,
+ 0.9866356,
+ 0.99343646,
+ 0.99050206,
+ 0.9866356
+ ],
+ [
+ 0.99481374,
+ 0.9920071,
+ 0.98856884,
+ 0.99481374,
+ 0.9920071,
+ 0.98856884,
+ 0.99481374,
+ 0.9920071,
+ 0.98856884
+ ],
+ [
+ 0.99578595,
+ 0.99343646,
+ 0.99050206,
+ 0.99578595,
+ 0.99343646,
+ 0.99050206,
+ 0.99578595,
+ 0.99343646,
+ 0.99050206
+ ],
+ [
+ 0.9967581,
+ 0.99481374,
+ 0.9920071,
+ 0.9967581,
+ 0.99481374,
+ 0.9920071,
+ 0.9967581,
+ 0.99481374,
+ 0.9920071
+ ],
+ [
+ 0.9976717,
+ 0.99578595,
+ 0.99343646,
+ 0.9976717,
+ 0.99578595,
+ 0.99343646,
+ 0.9976717,
+ 0.99578595,
+ 0.99343646
+ ],
+ [
+ 0.99822795,
+ 0.9967581,
+ 0.99481374,
+ 0.99822795,
+ 0.9967581,
+ 0.99481374,
+ 0.99822795,
+ 0.9967581,
+ 0.99481374
+ ],
+ [
+ 0.99878407,
+ 0.9976717,
+ 0.99578595,
+ 0.99878407,
+ 0.9976717,
+ 0.99578595,
+ 0.99878407,
+ 0.9976717,
+ 0.99578595
+ ],
+ [
+ 0.9993403,
+ 0.99822795,
+ 0.9967581,
+ 0.9993403,
+ 0.99822795,
+ 0.9967581,
+ 0.9993403,
+ 0.99822795,
+ 0.9967581
+ ],
+ [
+ 0.99954754,
+ 0.99878407,
+ 0.9976717,
+ 0.99954754,
+ 0.99878407,
+ 0.9976717,
+ 0.99954754,
+ 0.99878407,
+ 0.9976717
+ ],
+ [
+ 0.9997241,
+ 0.9993403,
+ 0.99822795,
+ 0.9997241,
+ 0.9993403,
+ 0.99822795,
+ 0.9997241,
+ 0.9993403,
+ 0.99822795
+ ],
+ [
+ 0.9999007,
+ 0.99954754,
+ 0.99878407,
+ 0.9999007,
+ 0.99954754,
+ 0.99878407,
+ 0.9999007,
+ 0.99954754,
+ 0.99878407
+ ],
+ [
+ 1,
+ 0.9997241,
+ 0.9993403,
+ 1,
+ 0.9997241,
+ 0.9993403,
+ 1,
+ 0.9997241,
+ 0.9993403
+ ],
+ [
+ 1,
+ 0.9999007,
+ 0.99954754,
+ 1,
+ 0.9999007,
+ 0.99954754,
+ 1,
+ 0.9999007,
+ 0.99954754
+ ],
+ [
+ 1,
+ 1,
+ 0.9997241,
+ 1,
+ 1,
+ 0.9997241,
+ 1,
+ 1,
+ 0.9997241
+ ],
+ [
+ 1,
+ 1,
+ 0.9999007,
+ 1,
+ 1,
+ 0.9999007,
+ 1,
+ 1,
+ 0.9999007
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ]
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/goldens/doubleClick_swapSide.json b/packages/SystemUI/tests/goldens/doubleClick_swapSide.json
new file mode 100644
index 0000000..044ddbc
--- /dev/null
+++ b/packages/SystemUI/tests/goldens/doubleClick_swapSide.json
@@ -0,0 +1,195 @@
+{
+ "frame_ids": [
+ "before",
+ 0,
+ 16,
+ 32,
+ 48,
+ 64,
+ 80,
+ 96,
+ 112,
+ 128,
+ 144,
+ 160,
+ 176,
+ 192,
+ "after"
+ ],
+ "features": [
+ {
+ "name": "userSwitcher_pos",
+ "type": "dpOffset",
+ "data_points": [
+ {
+ "x": 0,
+ "y": 96
+ },
+ {
+ "x": 0,
+ "y": 96
+ },
+ {
+ "x": 0,
+ "y": 96
+ },
+ {
+ "x": 0,
+ "y": 96
+ },
+ {
+ "x": 45.008995,
+ "y": 96
+ },
+ {
+ "x": 123.20912,
+ "y": 96
+ },
+ {
+ "x": 194.33762,
+ "y": 96
+ },
+ {
+ "x": 248.24419,
+ "y": 96
+ },
+ {
+ "x": 285.66364,
+ "y": 96
+ },
+ {
+ "x": 310.326,
+ "y": 96
+ },
+ {
+ "x": 326.03296,
+ "y": 96
+ },
+ {
+ "x": 335.79584,
+ "y": 96
+ },
+ {
+ "x": 341.7547,
+ "y": 96
+ },
+ {
+ "x": 345.34082,
+ "y": 96
+ },
+ {
+ "x": 350.4762,
+ "y": 96
+ }
+ ]
+ },
+ {
+ "name": "userSwitcher_alpha",
+ "type": "float",
+ "data_points": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 0.44034958,
+ 0,
+ 0,
+ 0,
+ 0.24635443,
+ 0.49282384,
+ 0.67560554,
+ 0.7993379,
+ 0.8786727,
+ 0.9278107,
+ 1
+ ]
+ },
+ {
+ "name": "foldAware_pos",
+ "type": "dpOffset",
+ "data_points": [
+ {
+ "x": 350.4762,
+ "y": 96
+ },
+ {
+ "x": 350.4762,
+ "y": 96
+ },
+ {
+ "x": 350.4762,
+ "y": 96
+ },
+ {
+ "x": 350.4762,
+ "y": 96
+ },
+ {
+ "x": 305.4672,
+ "y": 96
+ },
+ {
+ "x": 227.26706,
+ "y": 96
+ },
+ {
+ "x": 156.13857,
+ "y": 96
+ },
+ {
+ "x": 102.232,
+ "y": 96
+ },
+ {
+ "x": 64.81257,
+ "y": 96
+ },
+ {
+ "x": 40.150204,
+ "y": 96
+ },
+ {
+ "x": 24.443243,
+ "y": 96
+ },
+ {
+ "x": 14.680362,
+ "y": 96
+ },
+ {
+ "x": 8.721494,
+ "y": 96
+ },
+ {
+ "x": 5.1353703,
+ "y": 96
+ },
+ {
+ "x": 0,
+ "y": 96
+ }
+ ]
+ },
+ {
+ "name": "foldAware_alpha",
+ "type": "float",
+ "data_points": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 0.44034958,
+ 0,
+ 0,
+ 0,
+ 0.24635443,
+ 0.49282384,
+ 0.67560554,
+ 0.7993379,
+ 0.8786727,
+ 0.9278107,
+ 1
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/goldens/entryAnimation.json b/packages/SystemUI/tests/goldens/entryAnimation.json
new file mode 100644
index 0000000..11bf09f
--- /dev/null
+++ b/packages/SystemUI/tests/goldens/entryAnimation.json
@@ -0,0 +1,823 @@
+{
+ "frame_ids": [
+ 0,
+ 16,
+ 32,
+ 48,
+ 64,
+ 80,
+ 96,
+ 112,
+ 128,
+ 144,
+ 160,
+ 176,
+ 192,
+ 208,
+ 224,
+ 240,
+ 256,
+ 272,
+ 288,
+ 304,
+ 320,
+ 336,
+ 352,
+ 368,
+ 384,
+ 400,
+ 416,
+ 432,
+ 448,
+ 464,
+ 480,
+ 496,
+ 512,
+ 528,
+ "after"
+ ],
+ "features": [
+ {
+ "name": "PinBouncer::dotAppearFadeIn",
+ "type": "float[]",
+ "data_points": [
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0.13006775,
+ 0,
+ 0,
+ 0.13006775,
+ 0,
+ 0,
+ 0.13006775,
+ 0,
+ 0
+ ],
+ [
+ 0.23268975,
+ 0,
+ 0,
+ 0.23268975,
+ 0,
+ 0,
+ 0.23268975,
+ 0,
+ 0
+ ],
+ [
+ 0.31893033,
+ 0.12296748,
+ 0,
+ 0.31893033,
+ 0.12296748,
+ 0,
+ 0.31893033,
+ 0.12296748,
+ 0
+ ],
+ [
+ 0.3938481,
+ 0.22658443,
+ 0,
+ 0.3938481,
+ 0.22658443,
+ 0,
+ 0.3938481,
+ 0.22658443,
+ 0
+ ],
+ [
+ 0.45988238,
+ 0.3139104,
+ 0.115867205,
+ 0.45988238,
+ 0.3139104,
+ 0.115867205,
+ 0.45988238,
+ 0.3139104,
+ 0.115867205
+ ],
+ [
+ 0.52037334,
+ 0.38916573,
+ 0.22036704,
+ 0.52037334,
+ 0.38916573,
+ 0.22036704,
+ 0.52037334,
+ 0.38916573,
+ 0.22036704
+ ],
+ [
+ 0.57470226,
+ 0.45587063,
+ 0.3084957,
+ 0.57470226,
+ 0.45587063,
+ 0.3084957,
+ 0.57470226,
+ 0.45587063,
+ 0.3084957
+ ],
+ [
+ 0.6230466,
+ 0.5169778,
+ 0.38448337,
+ 0.6230466,
+ 0.5169778,
+ 0.38448337,
+ 0.6230466,
+ 0.5169778,
+ 0.38448337
+ ],
+ [
+ 0.6682857,
+ 0.5713067,
+ 0.45185885,
+ 0.6682857,
+ 0.5713067,
+ 0.45185885,
+ 0.6682857,
+ 0.5713067,
+ 0.45185885
+ ],
+ [
+ 0.7079632,
+ 0.6202191,
+ 0.5135822,
+ 0.7079632,
+ 0.6202191,
+ 0.5135822,
+ 0.7079632,
+ 0.6202191,
+ 0.5135822
+ ],
+ [
+ 0.7447962,
+ 0.6654583,
+ 0.56791115,
+ 0.7447962,
+ 0.6654583,
+ 0.56791115,
+ 0.7447962,
+ 0.6654583,
+ 0.56791115
+ ],
+ [
+ 0.77875835,
+ 0.7056612,
+ 0.6173917,
+ 0.77875835,
+ 0.7056612,
+ 0.6173917,
+ 0.77875835,
+ 0.7056612,
+ 0.6173917
+ ],
+ [
+ 0.80779475,
+ 0.74249417,
+ 0.66263086,
+ 0.80779475,
+ 0.74249417,
+ 0.66263086,
+ 0.80779475,
+ 0.74249417,
+ 0.66263086
+ ],
+ [
+ 0.83683115,
+ 0.77694356,
+ 0.7033591,
+ 0.83683115,
+ 0.77694356,
+ 0.7033591,
+ 0.83683115,
+ 0.77694356,
+ 0.7033591
+ ],
+ [
+ 0.86171645,
+ 0.80597997,
+ 0.7401921,
+ 0.86171645,
+ 0.80597997,
+ 0.7401921,
+ 0.86171645,
+ 0.80597997,
+ 0.7401921
+ ],
+ [
+ 0.884127,
+ 0.83501637,
+ 0.7751288,
+ 0.884127,
+ 0.83501637,
+ 0.7751288,
+ 0.884127,
+ 0.83501637,
+ 0.7751288
+ ],
+ [
+ 0.9042124,
+ 0.86024225,
+ 0.8041652,
+ 0.9042124,
+ 0.86024225,
+ 0.8041652,
+ 0.9042124,
+ 0.86024225,
+ 0.8041652
+ ],
+ [
+ 0.9215058,
+ 0.8828717,
+ 0.8332016,
+ 0.9215058,
+ 0.8828717,
+ 0.8332016,
+ 0.9215058,
+ 0.8828717,
+ 0.8332016
+ ],
+ [
+ 0.9374617,
+ 0.9029571,
+ 0.8587681,
+ 0.9374617,
+ 0.9029571,
+ 0.8587681,
+ 0.9374617,
+ 0.9029571,
+ 0.8587681
+ ],
+ [
+ 0.95089734,
+ 0.92046183,
+ 0.88161635,
+ 0.95089734,
+ 0.92046183,
+ 0.88161635,
+ 0.95089734,
+ 0.92046183,
+ 0.88161635
+ ],
+ [
+ 0.9626156,
+ 0.93662196,
+ 0.90170175,
+ 0.9626156,
+ 0.93662196,
+ 0.90170175,
+ 0.9626156,
+ 0.93662196,
+ 0.90170175
+ ],
+ [
+ 0.97289115,
+ 0.95005757,
+ 0.91941786,
+ 0.97289115,
+ 0.95005757,
+ 0.91941786,
+ 0.97289115,
+ 0.95005757,
+ 0.91941786
+ ],
+ [
+ 0.98082036,
+ 0.96197337,
+ 0.9357822,
+ 0.98082036,
+ 0.96197337,
+ 0.9357822,
+ 0.98082036,
+ 0.96197337,
+ 0.9357822
+ ],
+ [
+ 0.98803866,
+ 0.9722489,
+ 0.94921786,
+ 0.98803866,
+ 0.9722489,
+ 0.94921786,
+ 0.98803866,
+ 0.9722489,
+ 0.94921786
+ ],
+ [
+ 0.99259704,
+ 0.98036927,
+ 0.9613311,
+ 0.99259704,
+ 0.98036927,
+ 0.9613311,
+ 0.99259704,
+ 0.98036927,
+ 0.9613311
+ ],
+ [
+ 0.99685574,
+ 0.9875875,
+ 0.97160673,
+ 0.99685574,
+ 0.9875875,
+ 0.97160673,
+ 0.99685574,
+ 0.9875875,
+ 0.97160673
+ ],
+ [
+ 0.9984336,
+ 0.99233085,
+ 0.9799181,
+ 0.9984336,
+ 0.99233085,
+ 0.9799181,
+ 0.9984336,
+ 0.99233085,
+ 0.9799181
+ ],
+ [
+ 0.99982595,
+ 0.99658954,
+ 0.98713636,
+ 0.99982595,
+ 0.99658954,
+ 0.98713636,
+ 0.99982595,
+ 0.99658954,
+ 0.98713636
+ ],
+ [
+ 1,
+ 0.99834657,
+ 0.99206465,
+ 1,
+ 0.99834657,
+ 0.99206465,
+ 1,
+ 0.99834657,
+ 0.99206465
+ ],
+ [
+ 1,
+ 0.99973893,
+ 0.9963234,
+ 1,
+ 0.99973893,
+ 0.9963234,
+ 1,
+ 0.99973893,
+ 0.9963234
+ ],
+ [
+ 1,
+ 1,
+ 0.99825954,
+ 1,
+ 1,
+ 0.99825954,
+ 1,
+ 1,
+ 0.99825954
+ ],
+ [
+ 1,
+ 1,
+ 0.9996519,
+ 1,
+ 1,
+ 0.9996519,
+ 1,
+ 1,
+ 0.9996519
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ]
+ ]
+ },
+ {
+ "name": "PinBouncer::dotAppearMoveUp",
+ "type": "float[]",
+ "data_points": [
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0.25108898,
+ 0.24185245,
+ 0.23379733,
+ 0.25108898,
+ 0.24185245,
+ 0.23379733,
+ 0.25108898,
+ 0.24185245,
+ 0.23379733
+ ],
+ [
+ 0.36994478,
+ 0.35579002,
+ 0.34344575,
+ 0.36994478,
+ 0.35579002,
+ 0.34344575,
+ 0.36994478,
+ 0.35579002,
+ 0.34344575
+ ],
+ [
+ 0.45856017,
+ 0.44208717,
+ 0.4277212,
+ 0.45856017,
+ 0.44208717,
+ 0.4277212,
+ 0.45856017,
+ 0.44208717,
+ 0.4277212
+ ],
+ [
+ 0.5303175,
+ 0.5132119,
+ 0.49780974,
+ 0.5303175,
+ 0.5132119,
+ 0.49780974,
+ 0.5303175,
+ 0.5132119,
+ 0.49780974
+ ],
+ [
+ 0.5929084,
+ 0.57152635,
+ 0.5528793,
+ 0.5929084,
+ 0.57152635,
+ 0.5528793,
+ 0.5929084,
+ 0.57152635,
+ 0.5528793
+ ],
+ [
+ 0.6415321,
+ 0.6216319,
+ 0.6042771,
+ 0.6415321,
+ 0.6216319,
+ 0.6042771,
+ 0.6415321,
+ 0.6216319,
+ 0.6042771
+ ],
+ [
+ 0.6889181,
+ 0.6668597,
+ 0.64661235,
+ 0.6889181,
+ 0.6668597,
+ 0.64661235,
+ 0.6889181,
+ 0.6668597,
+ 0.64661235
+ ],
+ [
+ 0.72852343,
+ 0.70699567,
+ 0.6879909,
+ 0.72852343,
+ 0.70699567,
+ 0.6879909,
+ 0.72852343,
+ 0.70699567,
+ 0.6879909
+ ],
+ [
+ 0.763233,
+ 0.7418899,
+ 0.7227609,
+ 0.763233,
+ 0.7418899,
+ 0.7227609,
+ 0.763233,
+ 0.7418899,
+ 0.7227609
+ ],
+ [
+ 0.7938958,
+ 0.7733934,
+ 0.75354666,
+ 0.7938958,
+ 0.7733934,
+ 0.75354666,
+ 0.7938958,
+ 0.7733934,
+ 0.75354666
+ ],
+ [
+ 0.82150793,
+ 0.8013512,
+ 0.7816832,
+ 0.82150793,
+ 0.8013512,
+ 0.7816832,
+ 0.82150793,
+ 0.8013512,
+ 0.7816832
+ ],
+ [
+ 0.84668195,
+ 0.82613826,
+ 0.807758,
+ 0.84668195,
+ 0.82613826,
+ 0.807758,
+ 0.84668195,
+ 0.82613826,
+ 0.807758
+ ],
+ [
+ 0.86843777,
+ 0.84911424,
+ 0.83017635,
+ 0.86843777,
+ 0.84911424,
+ 0.83017635,
+ 0.86843777,
+ 0.84911424,
+ 0.83017635
+ ],
+ [
+ 0.88804924,
+ 0.86938363,
+ 0.85123545,
+ 0.88804924,
+ 0.86938363,
+ 0.85123545,
+ 0.88804924,
+ 0.86938363,
+ 0.85123545
+ ],
+ [
+ 0.90616417,
+ 0.8875992,
+ 0.87020856,
+ 0.90616417,
+ 0.8875992,
+ 0.87020856,
+ 0.90616417,
+ 0.8875992,
+ 0.87020856
+ ],
+ [
+ 0.92120105,
+ 0.90447646,
+ 0.8872067,
+ 0.92120105,
+ 0.90447646,
+ 0.8872067,
+ 0.92120105,
+ 0.90447646,
+ 0.8872067
+ ],
+ [
+ 0.93561906,
+ 0.91881925,
+ 0.9030046,
+ 0.93561906,
+ 0.91881925,
+ 0.9030046,
+ 0.93561906,
+ 0.91881925,
+ 0.9030046
+ ],
+ [
+ 0.9472463,
+ 0.9325603,
+ 0.91674215,
+ 0.9472463,
+ 0.9325603,
+ 0.91674215,
+ 0.9472463,
+ 0.9325603,
+ 0.91674215
+ ],
+ [
+ 0.95841366,
+ 0.9437798,
+ 0.9296044,
+ 0.95841366,
+ 0.9437798,
+ 0.9296044,
+ 0.95841366,
+ 0.9437798,
+ 0.9296044
+ ],
+ [
+ 0.9671384,
+ 0.9546126,
+ 0.9407567,
+ 0.9671384,
+ 0.9546126,
+ 0.9407567,
+ 0.9671384,
+ 0.9546126,
+ 0.9407567
+ ],
+ [
+ 0.97568256,
+ 0.96334505,
+ 0.95089674,
+ 0.97568256,
+ 0.96334505,
+ 0.95089674,
+ 0.97568256,
+ 0.96334505,
+ 0.95089674
+ ],
+ [
+ 0.98170155,
+ 0.9714737,
+ 0.9600369,
+ 0.98170155,
+ 0.9714737,
+ 0.9600369,
+ 0.98170155,
+ 0.9714737,
+ 0.9600369
+ ],
+ [
+ 0.9877205,
+ 0.9782621,
+ 0.96764565,
+ 0.9877205,
+ 0.9782621,
+ 0.96764565,
+ 0.9877205,
+ 0.9782621,
+ 0.96764565
+ ],
+ [
+ 0.9916518,
+ 0.98386985,
+ 0.9752545,
+ 0.9916518,
+ 0.98386985,
+ 0.9752545,
+ 0.9916518,
+ 0.98386985,
+ 0.9752545
+ ],
+ [
+ 0.99514234,
+ 0.98918015,
+ 0.9805117,
+ 0.99514234,
+ 0.98918015,
+ 0.9805117,
+ 0.99514234,
+ 0.98918015,
+ 0.9805117
+ ],
+ [
+ 0.9976143,
+ 0.99243224,
+ 0.98576087,
+ 0.9976143,
+ 0.99243224,
+ 0.98576087,
+ 0.9976143,
+ 0.99243224,
+ 0.98576087
+ ],
+ [
+ 0.998737,
+ 0.9956844,
+ 0.9900688,
+ 0.998737,
+ 0.9956844,
+ 0.9900688,
+ 0.998737,
+ 0.9956844,
+ 0.9900688
+ ],
+ [
+ 0.9998597,
+ 0.99771196,
+ 0.9931129,
+ 0.9998597,
+ 0.99771196,
+ 0.9931129,
+ 0.9998597,
+ 0.99771196,
+ 0.9931129
+ ],
+ [
+ 1,
+ 0.9987579,
+ 0.99615705,
+ 1,
+ 0.9987579,
+ 0.99615705,
+ 1,
+ 0.9987579,
+ 0.99615705
+ ],
+ [
+ 1,
+ 0.9998039,
+ 0.9977971,
+ 1,
+ 0.9998039,
+ 0.9977971,
+ 1,
+ 0.9998039,
+ 0.9977971
+ ],
+ [
+ 1,
+ 1,
+ 0.99877614,
+ 1,
+ 1,
+ 0.99877614,
+ 1,
+ 1,
+ 0.99877614
+ ],
+ [
+ 1,
+ 1,
+ 0.9997552,
+ 1,
+ 1,
+ 0.9997552,
+ 1,
+ 1,
+ 0.9997552
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ]
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
new file mode 100644
index 0000000..9a36678
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.ui.composable
+
+import android.app.AlertDialog
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.doubleClick
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.android.compose.theme.PlatformTheme
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.ui.BouncerDialogFactory
+import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout
+import com.android.systemui.bouncer.ui.viewmodel.bouncerViewModel
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.motion.createSysUiComposeMotionTestRule
+import com.android.systemui.scene.domain.interactor.sceneContainerStartable
+import com.android.systemui.testKosmos
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.motion.compose.ComposeFeatureCaptures.alpha
+import platform.test.motion.compose.ComposeFeatureCaptures.positionInRoot
+import platform.test.motion.compose.ComposeRecordingSpec
+import platform.test.motion.compose.MotionControl
+import platform.test.motion.compose.feature
+import platform.test.motion.compose.motionTestValueOfNode
+import platform.test.motion.compose.recordMotion
+import platform.test.motion.compose.runTest
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.Displays.FoldableInner
+
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+class BouncerContentTest : SysuiTestCase() {
+ private val deviceSpec = DeviceEmulationSpec(FoldableInner)
+ private val kosmos = testKosmos()
+
+ @get:Rule val motionTestRule = createSysUiComposeMotionTestRule(kosmos, deviceSpec)
+
+ private val bouncerDialogFactory =
+ object : BouncerDialogFactory {
+ override fun invoke(): AlertDialog {
+ throw AssertionError()
+ }
+ }
+
+ @Before
+ fun setUp() {
+ kosmos.sceneContainerStartable.start()
+ kosmos.fakeFeatureFlagsClassic.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ }
+
+ @Composable
+ private fun BouncerContentUnderTest() {
+ PlatformTheme {
+ BouncerContent(
+ viewModel = kosmos.bouncerViewModel,
+ layout = BouncerSceneLayout.BESIDE_USER_SWITCHER,
+ modifier = Modifier.fillMaxSize().testTag("BouncerContent"),
+ dialogFactory = bouncerDialogFactory
+ )
+ }
+ }
+
+ @Test
+ fun doubleClick_swapSide() =
+ motionTestRule.runTest {
+ val motion =
+ recordMotion(
+ content = { BouncerContentUnderTest() },
+ ComposeRecordingSpec(
+ MotionControl {
+ onNode(hasTestTag("BouncerContent")).performTouchInput {
+ doubleClick(position = centerLeft)
+ }
+
+ awaitCondition {
+ motionTestValueOfNode(BouncerMotionTestKeys.swapAnimationEnd)
+ }
+ }
+ ) {
+ feature(hasTestTag("UserSwitcher"), positionInRoot, "userSwitcher_pos")
+ feature(hasTestTag("UserSwitcher"), alpha, "userSwitcher_alpha")
+ feature(hasTestTag("FoldAware"), positionInRoot, "foldAware_pos")
+ feature(hasTestTag("FoldAware"), alpha, "foldAware_alpha")
+ }
+ )
+
+ assertThat(motion).timeSeriesMatchesGolden()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt
new file mode 100644
index 0000000..4f6f98e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.ui.composable
+
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
+import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.motion.createSysUiComposeMotionTestRule
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.takeWhile
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.motion.compose.ComposeRecordingSpec
+import platform.test.motion.compose.MotionControl
+import platform.test.motion.compose.feature
+import platform.test.motion.compose.motionTestValueOfNode
+import platform.test.motion.compose.recordMotion
+import platform.test.motion.compose.runTest
+import platform.test.motion.golden.DataPointTypes
+
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+class PatternBouncerTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ @get:Rule val motionTestRule = createSysUiComposeMotionTestRule(kosmos)
+
+ private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
+ private val viewModel by lazy {
+ PatternBouncerViewModel(
+ applicationContext = context,
+ viewModelScope = kosmos.testScope.backgroundScope,
+ interactor = bouncerInteractor,
+ isInputEnabled = MutableStateFlow(true).asStateFlow(),
+ onIntentionalUserInput = {},
+ )
+ }
+
+ @Composable
+ private fun PatternBouncerUnderTest() {
+ PatternBouncer(viewModel, centerDotsVertically = true, modifier = Modifier.size(400.dp))
+ }
+
+ @Test
+ fun entryAnimation() =
+ motionTestRule.runTest {
+ val motion =
+ recordMotion(
+ content = { play -> if (play) PatternBouncerUnderTest() },
+ ComposeRecordingSpec.until(
+ recordBefore = false,
+ checkDone = { motionTestValueOfNode(MotionTestKeys.entryCompleted) }
+ ) {
+ feature(MotionTestKeys.dotAppearFadeIn, floatArray)
+ feature(MotionTestKeys.dotAppearMoveUp, floatArray)
+ }
+ )
+
+ assertThat(motion).timeSeriesMatchesGolden()
+ }
+
+ @Test
+ fun animateFailure() =
+ motionTestRule.runTest {
+ val failureAnimationMotionControl =
+ MotionControl(
+ delayReadyToPlay = {
+ // Skip entry animation.
+ awaitCondition { motionTestValueOfNode(MotionTestKeys.entryCompleted) }
+ },
+ delayRecording = {
+ // Trigger failure animation by calling onDragEnd without having recorded a
+ // pattern before.
+ viewModel.onDragEnd()
+ // Failure animation starts when animateFailure flips to true...
+ viewModel.animateFailure.takeWhile { !it }.collect {}
+ }
+ ) {
+ // ... and ends when the composable flips it back to false.
+ viewModel.animateFailure.takeWhile { it }.collect {}
+ }
+
+ val motion =
+ recordMotion(
+ content = { PatternBouncerUnderTest() },
+ ComposeRecordingSpec(failureAnimationMotionControl) {
+ feature(MotionTestKeys.dotScaling, floatArray)
+ }
+ )
+ assertThat(motion).timeSeriesMatchesGolden()
+ }
+
+ companion object {
+ val floatArray = DataPointTypes.listOf(DataPointTypes.float)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/motion/ComposeMotionTestRuleHelper.kt b/packages/SystemUI/tests/src/com/android/systemui/motion/ComposeMotionTestRuleHelper.kt
new file mode 100644
index 0000000..e81e42b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/motion/ComposeMotionTestRuleHelper.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.motion
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import org.junit.rules.RuleChain
+import platform.test.motion.MotionTestRule
+import platform.test.motion.compose.ComposeToolkit
+import platform.test.motion.testing.createGoldenPathManager
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.Displays
+import platform.test.screenshot.PathConfig
+import platform.test.screenshot.utils.compose.ComposeScreenshotTestRule
+
+/** Create a [MotionTestRule] for motion tests of Compose-based System UI. */
+fun createSysUiComposeMotionTestRule(
+ kosmos: Kosmos,
+ deviceEmulationSpec: DeviceEmulationSpec = DeviceEmulationSpec(Displays.Phone),
+ pathConfig: PathConfig = PathConfig(),
+): MotionTestRule<ComposeToolkit> {
+ val goldenPathManager =
+ createGoldenPathManager("frameworks/base/packages/SystemUI/tests/goldens", pathConfig)
+ val testScope = kosmos.testScope
+
+ val composeScreenshotTestRule =
+ ComposeScreenshotTestRule(deviceEmulationSpec, goldenPathManager)
+
+ return MotionTestRule(
+ ComposeToolkit(composeScreenshotTestRule.composeRule, testScope),
+ goldenPathManager,
+ bitmapDiffer = composeScreenshotTestRule,
+ extraRules = RuleChain.outerRule(composeScreenshotTestRule)
+ )
+}