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