1 /*
2  * Copyright 2023 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 androidx.compose.ui.test
18 
19 import android.os.Build
20 import androidx.test.espresso.AppNotIdleException
21 import androidx.test.espresso.IdlingPolicies
22 import kotlin.coroutines.CoroutineContext
23 import kotlinx.coroutines.Dispatchers
24 
25 /**
26  * Whether or not this test is running on Robolectric.
27  *
28  * The implementation of this check is widely used but not officially supported and should therefore
29  * stay internal.
30  */
31 internal val HasRobolectricFingerprint
32     get() = Build.FINGERPRINT.lowercase() == "robolectric"
33 
34 /**
35  * Idling strategy for use with Robolectric.
36  *
37  * When running on Robolectric, the following things are different:
38  * 1. IdlingResources are not queried. We drive Compose from the ComposeIdlingResource, so we need
39  *    to do that manually here.
40  * 2. Draw passes don't happen. Compose performs most measure and layout passes during the draw
41  *    pass, so we need to manually trigger an actual measure/layout pass when needed.
42  * 3. Awaiting idleness must happen on the main thread. On Espresso it's exactly the other way
43  *    around, so we need to invert our thread checks.
44  *
45  * Note that we explicitly don't install our [IdlingResourceRegistry] into Espresso even though it
46  * would be a noop anyway: if at some point in the future they will be supported, our behavior would
47  * silently change (potentially leading to breakages).
48  */
49 internal class RobolectricIdlingStrategy(
50     private val composeRootRegistry: ComposeRootRegistry,
51     private val composeIdlingResource: ComposeIdlingResource
52 ) : IdlingStrategy {
53     override val canSynchronizeOnUiThread: Boolean = true
54 
55     /*
56      * On Robolectric, Espresso.onIdle() needs to be called from the main thread; so use
57      * Dispatchers.Main. Use `.immediate` in case we're already on the main thread.
58      */
59     override val synchronizationContext: CoroutineContext
60         get() = Dispatchers.Main.immediate
61 
runUntilIdlenull62     override fun runUntilIdle() {
63         val policy = IdlingPolicies.getMasterIdlingPolicy()
64         val timeoutMillis = policy.idleTimeoutUnit.toMillis(policy.idleTimeout)
65         runOnUiThread {
66             // Use Java's clock, Android's clock is mocked
67             val start = System.currentTimeMillis()
68             var iteration = 0
69             do {
70                 // Check if we hit the timeout
71                 if (System.currentTimeMillis() - start >= timeoutMillis) {
72                     throw AppNotIdleException.create(
73                         emptyList(),
74                         "Compose did not get idle after $iteration attempts in " +
75                             "${policy.idleTimeout} ${policy.idleTimeoutUnit}. " +
76                             "Please check your measure/layout lambdas, they may be " +
77                             "causing an infinite composition loop. Or set Espresso's " +
78                             "master idling policy if you require a longer timeout."
79                     )
80                 }
81                 iteration++
82                 // Run Espresso.onIdle() to drain the main message queue
83                 runEspressoOnIdle()
84                 // Check if we need a measure/layout pass
85                 requestLayoutIfNeeded()
86                 // Let ComposeIdlingResource fast-forward compositions
87                 val isIdle = composeIdlingResource.isIdleNow
88                 // Repeat while not idle
89             } while (!isIdle)
90         }
91     }
92 
93     /**
94      * Calls [requestLayout][android.view.View.requestLayout] on all compose hosts that are awaiting
95      * a measure/layout pass, because the draw pass that it is normally awaiting never happens on
96      * Robolectric.
97      */
requestLayoutIfNeedednull98     private fun requestLayoutIfNeeded(): Boolean {
99         val composeRoots = composeRootRegistry.getRegisteredComposeRoots()
100         return composeRoots
101             .filter { it.shouldWaitForMeasureAndLayout }
102             .onEach { it.view.requestLayout() }
103             .isNotEmpty()
104     }
105 }
106