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