1 /* 2 * Copyright 2020 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 androidx.compose.ui.node.RootForTest 20 import androidx.compose.ui.semantics.SemanticsNode 21 import androidx.compose.ui.semantics.getAllSemanticsNodes 22 23 /** 24 * Provides necessary services to facilitate testing. 25 * 26 * This is typically implemented by entities like test rule. 27 */ 28 internal interface TestOwner { 29 /** Clock that drives frames and recompositions in compose tests. */ 30 val mainClock: MainTestClock 31 32 /** 33 * Runs the given [action] on the ui thread. 34 * 35 * This is a blocking call. 36 */ 37 // TODO: Does ui-test really need it? Can it use coroutine context on Owner? runOnUiThreadnull38 fun <T> runOnUiThread(action: () -> T): T 39 40 /** 41 * Collects all [RootForTest]s from all compose hierarchies. 42 * 43 * This method is the choke point where all assertions and interactions must go through when 44 * testing composables and where we thus have the opportunity to automatically reach quiescence. 45 * This is done by calling [ComposeUiTest.waitForIdle] before getting and returning the 46 * registered roots. 47 * 48 * @param atLeastOneRootExpected Whether the caller expects that at least one compose root is 49 * present in the tested app. This affects synchronization efforts / timeouts of this API. 50 */ 51 fun getRoots(atLeastOneRootExpected: Boolean): Set<RootForTest> 52 } 53 54 /** 55 * Collects all [SemanticsNode]s from all compose hierarchies, and returns the [transform]ed 56 * results. 57 * 58 * Set [useUnmergedTree] to `true` to search through the unmerged semantics tree. 59 * 60 * Set [skipDeactivatedNodes] to `false` to include 61 * [deactivated][androidx.compose.ui.node.LayoutNode.isDeactivated] nodes in the search. 62 * 63 * Use [atLeastOneRootRequired] to treat not finding any compose hierarchies at all as an error. If 64 * no hierarchies are found, we will wait 2 seconds to accommodate cases where composable content is 65 * set asynchronously. On the other hand, if you expect or know that there is no composable content, 66 * set [atLeastOneRootRequired] to `false` and no error will be thrown if there are no compose 67 * roots, and the wait for compose roots will be reduced to .5 seconds. 68 * 69 * This method will wait for quiescence before collecting all SemanticsNodes. Collection happens on 70 * the main thread and the [transform]ation of all SemanticsNodes to a result is done while on the 71 * main thread. This allows us to transform the result using methods that must be called on the main 72 * thread, without switching back and forth between the main thread and the test thread. 73 */ 74 internal fun <R> TestOwner.getAllSemanticsNodes( 75 atLeastOneRootRequired: Boolean, 76 useUnmergedTree: Boolean, 77 skipDeactivatedNodes: Boolean = true, 78 transform: (Iterable<SemanticsNode>) -> R 79 ): R { 80 val roots = 81 getRoots(atLeastOneRootRequired).also { 82 check(!atLeastOneRootRequired || it.isNotEmpty()) { 83 "No compose hierarchies found in the app. Possible reasons include: " + 84 "(1) the Activity that calls setContent did not launch; " + 85 "(2) setContent was not called; " + 86 "(3) setContent was called before the ComposeTestRule ran. " + 87 "If setContent is called by the Activity, make sure the Activity is " + 88 "launched after the ComposeTestRule runs" 89 } 90 } 91 92 return runOnUiThread { 93 transform.invoke( 94 roots.flatMap { 95 it.semanticsOwner.getAllSemanticsNodes( 96 mergingEnabled = !useUnmergedTree, 97 skipDeactivatedNodes = skipDeactivatedNodes 98 ) 99 } 100 ) 101 } 102 } 103