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)
+    )
+}