• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2025 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package platform.test.motion.compose
18 
19 import android.content.res.Configuration
20 import android.util.DisplayMetrics
21 import android.view.ContextThemeWrapper
22 import androidx.compose.runtime.Composable
23 import androidx.compose.runtime.CompositionLocalProvider
24 import androidx.compose.runtime.remember
25 import androidx.compose.ui.platform.LocalConfiguration
26 import androidx.compose.ui.platform.LocalContext
27 import androidx.compose.ui.platform.LocalDensity
28 import androidx.compose.ui.platform.LocalViewConfiguration
29 import androidx.compose.ui.platform.ViewConfiguration
30 import androidx.compose.ui.test.ExperimentalTestApi
31 import androidx.compose.ui.test.junit4.ComposeContentTestRule
32 import androidx.compose.ui.test.junit4.createComposeRule
33 import androidx.compose.ui.unit.Density
34 import androidx.compose.ui.unit.Dp
35 import androidx.compose.ui.unit.dp
36 import kotlin.math.floor
37 import kotlinx.coroutines.Dispatchers
38 import kotlinx.coroutines.test.TestScope
39 import org.junit.rules.RuleChain
40 import platform.test.motion.MotionTestRule
41 import platform.test.screenshot.GoldenPathManager
42 
43 /**
44  * @param density Sets the [LocalDensity], allowing for a deterministic calculation of `dp -> px ->
45  *   dp`.
46  * @param touchSlop Sets the [ViewConfiguration.touchSlop], allowing for a deterministic gesture
47  *   start.
48  */
49 data class FixedConfiguration(
50     val density: Density = DefaultDensity,
51     val touchSlop: Dp = DefaultTouchSlop,
52 ) {
53 
54     companion object {
55         /**
56          * Default density for tests.
57          *
58          * Using a uneven number to reveal issues with rounding.
59          */
60         val DefaultDensity = Density(2.5f)
61 
62         /**
63          * Touch slop for gestures.
64          *
65          * Using the cuttlefish default value.
66          */
67         val DefaultTouchSlop = 12.dp
68     }
69 }
70 
71 /**
72  * Convenience to create a [MotionTestRule], including the required setup.
73  *
74  * NOTE: The [configuration] applies to the complete content, EXCEPT the root node returned by the
75  * `isRoot()` [SemanticMatcher]. This can produce unexpected results when dispatching gestures on
76  * the root node. To work around this, dispatch the gestures on a node owned by the composable under
77  * test.
78  *
79  * In addition to the [MotionTestRule], this function also creates a [ComposeContentTestRule], which
80  * is run as part of the [MotionTestRule].
81  */
82 @OptIn(ExperimentalTestApi::class)
createFixedConfigurationComposeMotionTestRulenull83 fun createFixedConfigurationComposeMotionTestRule(
84     goldenPathManager: GoldenPathManager,
85     testScope: TestScope = TestScope(),
86     configuration: FixedConfiguration = FixedConfiguration(),
87 ): MotionTestRule<ComposeToolkit> {
88     val composeRule = createComposeRule(testScope.coroutineContext + Dispatchers.Main)
89 
90     return MotionTestRule(
91         ComposeToolkit(composeRule, testScope, configuration),
92         goldenPathManager,
93         extraRules = RuleChain.outerRule(composeRule),
94     )
95 }
96 
97 @Composable
FixedConfigurationProvidernull98 internal fun FixedConfigurationProvider(
99     fixedConfiguration: FixedConfiguration,
100     content: @Composable () -> Unit,
101 ) {
102     val baseLocalConfiguration = LocalConfiguration.current
103     val configuration =
104         remember(baseLocalConfiguration, fixedConfiguration) {
105             Configuration().apply {
106                 updateFrom(baseLocalConfiguration)
107                 densityDpi =
108                     floor(fixedConfiguration.density.density * DisplayMetrics.DENSITY_DEFAULT)
109                         .toInt()
110                 fontScale = fixedConfiguration.density.fontScale
111             }
112         }
113     val previousContext = LocalContext.current
114     val context =
115         remember(previousContext, configuration) {
116             ContextThemeWrapper(previousContext, 0).apply {
117                 applyOverrideConfiguration(configuration)
118             }
119         }
120 
121     val baseViewConfiguration = LocalViewConfiguration.current
122     val customViewConfiguration =
123         remember(baseViewConfiguration, fixedConfiguration) {
124             object : ViewConfiguration by baseViewConfiguration {
125                 override val touchSlop: Float =
126                     with(fixedConfiguration.density) { fixedConfiguration.touchSlop.toPx() }
127             }
128         }
129 
130     CompositionLocalProvider(
131         LocalContext provides context,
132         LocalDensity provides fixedConfiguration.density,
133         LocalConfiguration provides configuration,
134         LocalViewConfiguration provides customViewConfiguration,
135         content = content,
136     )
137 }
138