1 /*
<lambda>null2  * 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.junit4
18 
19 import androidx.activity.ComponentActivity
20 import androidx.annotation.RestrictTo
21 import androidx.compose.runtime.Composable
22 import androidx.compose.ui.test.AndroidComposeUiTestEnvironment
23 import androidx.compose.ui.test.ComposeAccessibilityValidator
24 import androidx.compose.ui.test.ExperimentalTestApi
25 import androidx.compose.ui.test.IdlingResource
26 import androidx.compose.ui.test.MainTestClock
27 import androidx.compose.ui.test.SemanticsMatcher
28 import androidx.compose.ui.test.SemanticsNodeInteraction
29 import androidx.compose.ui.test.SemanticsNodeInteractionCollection
30 import androidx.compose.ui.test.waitUntilAtLeastOneExists
31 import androidx.compose.ui.test.waitUntilDoesNotExist
32 import androidx.compose.ui.test.waitUntilExactlyOneExists
33 import androidx.compose.ui.test.waitUntilNodeCount
34 import androidx.compose.ui.unit.Density
35 import androidx.test.ext.junit.rules.ActivityScenarioRule
36 import kotlin.coroutines.CoroutineContext
37 import kotlin.coroutines.EmptyCoroutineContext
38 import kotlin.time.Duration
39 import kotlinx.coroutines.test.TestCoroutineScheduler
40 import kotlinx.coroutines.test.TestDispatcher
41 import org.junit.rules.TestRule
42 import org.junit.runner.Description
43 import org.junit.runners.model.Statement
44 
45 actual fun createComposeRule(): ComposeContentTestRule =
46     createAndroidComposeRule<ComponentActivity>()
47 
48 @ExperimentalTestApi
49 actual fun createComposeRule(effectContext: CoroutineContext): ComposeContentTestRule =
50     createAndroidComposeRule<ComponentActivity>(effectContext)
51 
52 /**
53  * Factory method to provide android specific implementation of [createComposeRule], for a given
54  * activity class type [A].
55  *
56  * This method is useful for tests that require a custom Activity. This is usually the case for
57  * tests where the compose content is set by that Activity, instead of via the test rule's
58  * [setContent][ComposeContentTestRule.setContent]. Make sure that you add the provided activity
59  * into your app's manifest file (usually in main/AndroidManifest.xml).
60  *
61  * This creates a test rule that is using [ActivityScenarioRule] as the activity launcher. If you
62  * would like to use a different one you can create [AndroidComposeTestRule] directly and supply it
63  * with your own launcher.
64  *
65  * If your test doesn't require a specific Activity, use [createComposeRule] instead.
66  */
67 inline fun <reified A : ComponentActivity> createAndroidComposeRule():
68     AndroidComposeTestRule<ActivityScenarioRule<A>, A> {
69     // TODO(b/138993381): By launching custom activities we are losing control over what content is
70     //  already there. This is issue in case the user already set some compose content and decides
71     //  to set it again via our API. In such case we won't be able to dispose the old composition.
72     //  Other option would be to provide a smaller interface that does not expose these methods.
73     return createAndroidComposeRule(A::class.java)
74 }
75 
76 /**
77  * Factory method to provide android specific implementation of [createComposeRule], for a given
78  * activity class type [A].
79  *
80  * This method is useful for tests that require a custom Activity. This is usually the case for
81  * tests where the compose content is set by that Activity, instead of via the test rule's
82  * [setContent][ComposeContentTestRule.setContent]. Make sure that you add the provided activity
83  * into your app's manifest file (usually in main/AndroidManifest.xml).
84  *
85  * This creates a test rule that is using [ActivityScenarioRule] as the activity launcher. If you
86  * would like to use a different one you can create [AndroidComposeTestRule] directly and supply it
87  * with your own launcher.
88  *
89  * If your test doesn't require a specific Activity, use [createComposeRule] instead.
90  *
91  * @param effectContext The [CoroutineContext] used to run the composition. The context for
92  *   `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context. If this
93  *   context contains a [TestDispatcher] or [TestCoroutineScheduler] (in that order), it will be
94  *   used for composition and the [MainTestClock].
95  */
96 @ExperimentalTestApi
createAndroidComposeRulenull97 inline fun <reified A : ComponentActivity> createAndroidComposeRule(
98     effectContext: CoroutineContext = EmptyCoroutineContext
99 ): AndroidComposeTestRule<ActivityScenarioRule<A>, A> {
100     // TODO(b/138993381): By launching custom activities we are losing control over what content is
101     //  already there. This is issue in case the user already set some compose content and decides
102     //  to set it again via our API. In such case we won't be able to dispose the old composition.
103     //  Other option would be to provide a smaller interface that does not expose these methods.
104     return createAndroidComposeRule(A::class.java, effectContext)
105 }
106 
107 /**
108  * Factory method to provide android specific implementation of [createComposeRule], for a given
109  * [activityClass].
110  *
111  * This method is useful for tests that require a custom Activity. This is usually the case for
112  * tests where the compose content is set by that Activity, instead of via the test rule's
113  * [setContent][ComposeContentTestRule.setContent]. Make sure that you add the provided activity
114  * into your app's manifest file (usually in main/AndroidManifest.xml).
115  *
116  * This creates a test rule that is using [ActivityScenarioRule] as the activity launcher. If you
117  * would like to use a different one you can create [AndroidComposeTestRule] directly and supply it
118  * with your own launcher.
119  *
120  * If your test doesn't require a specific Activity, use [createComposeRule] instead.
121  */
createAndroidComposeRulenull122 fun <A : ComponentActivity> createAndroidComposeRule(
123     activityClass: Class<A>
124 ): AndroidComposeTestRule<ActivityScenarioRule<A>, A> =
125     AndroidComposeTestRule(
126         activityRule = ActivityScenarioRule(activityClass),
127         activityProvider = ::getActivityFromTestRule
128     )
129 
130 /**
131  * Factory method to provide android specific implementation of [createComposeRule], for a given
132  * [activityClass].
133  *
134  * This method is useful for tests that require a custom Activity. This is usually the case for
135  * tests where the compose content is set by that Activity, instead of via the test rule's
136  * [setContent][ComposeContentTestRule.setContent]. Make sure that you add the provided activity
137  * into your app's manifest file (usually in main/AndroidManifest.xml).
138  *
139  * This creates a test rule that is using [ActivityScenarioRule] as the activity launcher. If you
140  * would like to use a different one you can create [AndroidComposeTestRule] directly and supply it
141  * with your own launcher.
142  *
143  * If your test doesn't require a specific Activity, use [createComposeRule] instead.
144  *
145  * @param activityClass The activity type to use in the activity scenario
146  * @param effectContext The [CoroutineContext] used to run the composition. The context for
147  *   `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context. If this
148  *   context contains a [TestDispatcher] or [TestCoroutineScheduler] (in that order), it will be
149  *   used for composition and the [MainTestClock].
150  */
151 @ExperimentalTestApi
152 fun <A : ComponentActivity> createAndroidComposeRule(
153     activityClass: Class<A>,
154     effectContext: CoroutineContext = EmptyCoroutineContext
155 ): AndroidComposeTestRule<ActivityScenarioRule<A>, A> =
156     AndroidComposeTestRule(
157         activityRule = ActivityScenarioRule(activityClass),
158         activityProvider = ::getActivityFromTestRule,
159         effectContext = effectContext
160     )
161 
162 /**
163  * Factory method to provide an implementation of [ComposeTestRule] that doesn't create a compose
164  * host for you in which you can set content.
165  *
166  * This method is useful for tests that need to create their own compose host during the test. The
167  * returned test rule will not create a host, and consequently does not provide a `setContent`
168  * method. To set content in tests using this rule, use the appropriate `setContent` methods from
169  * your compose host.
170  *
171  * A typical use case on Android is when the test needs to launch an Activity (the compose host)
172  * after one or more dependencies have been injected.
173  */
174 fun createEmptyComposeRule(): ComposeTestRule =
175     AndroidComposeTestRule<TestRule, ComponentActivity>(
176         activityRule = TestRule { base, _ -> base },
<lambda>null177         activityProvider = {
178             error(
179                 "createEmptyComposeRule() does not provide an Activity to set Compose content in." +
180                     " Launch and use the Activity yourself, or use createAndroidComposeRule()."
181             )
182         }
183     )
184 
185 /**
186  * Factory method to provide an implementation of [ComposeTestRule] that doesn't create a compose
187  * host for you in which you can set content.
188  *
189  * This method is useful for tests that need to create their own compose host during the test. The
190  * returned test rule will not create a host, and consequently does not provide a `setContent`
191  * method. To set content in tests using this rule, use the appropriate `setContent` methods from
192  * your compose host.
193  *
194  * A typical use case on Android is when the test needs to launch an Activity (the compose host)
195  * after one or more dependencies have been injected.
196  *
197  * @param effectContext The [CoroutineContext] used to run the composition. The context for
198  *   `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context. If this
199  *   context contains a [TestDispatcher] or [TestCoroutineScheduler] (in that order), it will be
200  *   used for composition and the [MainTestClock].
201  */
202 @ExperimentalTestApi
createEmptyComposeRulenull203 fun createEmptyComposeRule(
204     effectContext: CoroutineContext = EmptyCoroutineContext
205 ): ComposeTestRule =
206     AndroidComposeTestRule<TestRule, ComponentActivity>(
207         activityRule = TestRule { base, _ -> base },
208         effectContext = effectContext,
<lambda>null209         activityProvider = {
210             error(
211                 "createEmptyComposeRule() does not provide an Activity to set Compose content in." +
212                     " Launch and use the Activity yourself, or use createAndroidComposeRule()."
213             )
214         }
215     )
216 
217 @OptIn(ExperimentalTestApi::class)
218 class AndroidComposeTestRule<R : TestRule, A : ComponentActivity>
219 private constructor(
220     val activityRule: R,
221     private val environment: AndroidComposeUiTestEnvironment<A>
222 ) : ComposeContentTestRule {
223     private val composeTest = environment.test
224 
225     /**
226      * Android specific implementation of [ComposeContentTestRule], where compose content is hosted
227      * by an Activity.
228      *
229      * The Activity is normally launched by the given [activityRule] before the test starts, but it
230      * is possible to pass a test rule that chooses to launch an Activity on a later time. The
231      * Activity is retrieved from the [activityRule] by means of the [activityProvider], which can
232      * be thought of as a getter for the Activity on the [activityRule]. If you use an
233      * [activityRule] that launches an Activity on a later time, you should make sure that the
234      * Activity is launched by the time or while the [activityProvider] is called.
235      *
236      * The [AndroidComposeTestRule] wraps around the given [activityRule] to make sure the Activity
237      * is launched _after_ the [AndroidComposeTestRule] has completed all necessary steps to control
238      * and monitor the compose content.
239      *
240      * @param activityRule Test rule to use to launch the Activity.
241      * @param activityProvider Function to retrieve the Activity from the given [activityRule].
242      */
243     constructor(
244         activityRule: R,
245         activityProvider: (R) -> A
246     ) : this(
247         activityRule = activityRule,
248         effectContext = EmptyCoroutineContext,
249         activityProvider = activityProvider,
250     )
251 
252     /**
253      * Android specific implementation of [ComposeContentTestRule], where compose content is hosted
254      * by an Activity.
255      *
256      * The Activity is normally launched by the given [activityRule] before the test starts, but it
257      * is possible to pass a test rule that chooses to launch an Activity on a later time. The
258      * Activity is retrieved from the [activityRule] by means of the [activityProvider], which can
259      * be thought of as a getter for the Activity on the [activityRule]. If you use an
260      * [activityRule] that launches an Activity on a later time, you should make sure that the
261      * Activity is launched by the time or while the [activityProvider] is called.
262      *
263      * The [AndroidComposeTestRule] wraps around the given [activityRule] to make sure the Activity
264      * is launched _after_ the [AndroidComposeTestRule] has completed all necessary steps to control
265      * and monitor the compose content.
266      *
267      * @param activityRule Test rule to use to launch the Activity.
268      * @param effectContext The [CoroutineContext] used to run the composition. The context for
269      *   `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context. If this
270      *   context contains a [TestDispatcher] or [TestCoroutineScheduler] (in that order), it will be
271      *   used for composition and the [MainTestClock].
272      * @param activityProvider Function to retrieve the Activity from the given [activityRule].
273      */
274     @ExperimentalTestApi
275     constructor(
276         activityRule: R,
277         effectContext: CoroutineContext = EmptyCoroutineContext,
278         activityProvider: (R) -> A,
279     ) : this(
280         activityRule,
281         AndroidComposeUiTestEnvironment(
282             effectContext = effectContext,
283             // Since now it calls kotlinx.coroutines.test.runTest under the hood,
284             // to preserve the behaviour compatibility we set an Infinite timeout
285             testTimeout = Duration.INFINITE
286         ) {
287             activityProvider(activityRule)
288         },
289     )
290 
291     /**
292      * Provides the current activity.
293      *
294      * Avoid calling often as it can involve synchronization and can be slow.
295      */
296     val activity: A
<lambda>null297         get() = checkNotNull(composeTest.activity) { "Host activity not found" }
298 
applynull299     override fun apply(base: Statement, description: Description): Statement {
300         val testWithDisposal =
301             object : Statement() {
302                 override fun evaluate() {
303                     var blockException: Throwable? = null
304                     try {
305                         // Run the test
306                         base.evaluate()
307                     } catch (t: Throwable) {
308                         blockException = t
309                     }
310 
311                     // Throw the aggregate exception. May be from the test body or from the cleanup.
312                     blockException?.let { throw it }
313                 }
314             }
315 
316         return object : Statement() {
317             override fun evaluate() {
318                 environment.runTest { activityRule.apply(testWithDisposal, description).evaluate() }
319             }
320         }
321     }
322 
323     @Deprecated(
324         message = "Do not instantiate this Statement, use AndroidComposeTestRule instead",
325         level = DeprecationLevel.ERROR
326     )
327     inner class AndroidComposeStatement(private val base: Statement) : Statement() {
evaluatenull328         override fun evaluate() {
329             base.evaluate()
330         }
331     }
332 
333     /*
334      * WHEN THE NAME AND SHAPE OF THE NEW COMMON INTERFACES HAS BEEN DECIDED,
335      * REPLACE ALL OVERRIDES BELOW WITH DELEGATION: ComposeTest by composeTest
336      */
337 
338     override val density: Density
339         get() = composeTest.density
340 
341     override val mainClock: MainTestClock
342         get() = composeTest.mainClock
343 
344     /**
345      * Sets the [ComposeAccessibilityValidator] to perform the accessibility checks with. Providing
346      * `null` means disabling the accessibility checks
347      */
348     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
setComposeAccessibilityValidatornull349     fun setComposeAccessibilityValidator(validator: ComposeAccessibilityValidator?) {
350         composeTest.setComposeAccessibilityValidator(validator)
351     }
352 
runOnUiThreadnull353     override fun <T> runOnUiThread(action: () -> T): T = composeTest.runOnUiThread(action)
354 
355     override fun <T> runOnIdle(action: () -> T): T = composeTest.runOnIdle(action)
356 
357     override fun waitForIdle() = composeTest.waitForIdle()
358 
359     override suspend fun awaitIdle() = composeTest.awaitIdle()
360 
361     override fun waitUntil(timeoutMillis: Long, condition: () -> Boolean) =
362         composeTest.waitUntil(conditionDescription = null, timeoutMillis, condition)
363 
364     override fun waitUntil(
365         conditionDescription: String,
366         timeoutMillis: Long,
367         condition: () -> Boolean
368     ) {
369         composeTest.waitUntil(conditionDescription, timeoutMillis, condition)
370     }
371 
372     @ExperimentalTestApi
waitUntilNodeCountnull373     override fun waitUntilNodeCount(matcher: SemanticsMatcher, count: Int, timeoutMillis: Long) =
374         composeTest.waitUntilNodeCount(matcher, count, timeoutMillis)
375 
376     @ExperimentalTestApi
377     override fun waitUntilAtLeastOneExists(matcher: SemanticsMatcher, timeoutMillis: Long) =
378         composeTest.waitUntilAtLeastOneExists(matcher, timeoutMillis)
379 
380     @ExperimentalTestApi
381     override fun waitUntilExactlyOneExists(matcher: SemanticsMatcher, timeoutMillis: Long) =
382         composeTest.waitUntilExactlyOneExists(matcher, timeoutMillis)
383 
384     @ExperimentalTestApi
385     override fun waitUntilDoesNotExist(matcher: SemanticsMatcher, timeoutMillis: Long) =
386         composeTest.waitUntilDoesNotExist(matcher, timeoutMillis)
387 
388     override fun registerIdlingResource(idlingResource: IdlingResource) =
389         composeTest.registerIdlingResource(idlingResource)
390 
391     override fun unregisterIdlingResource(idlingResource: IdlingResource) =
392         composeTest.unregisterIdlingResource(idlingResource)
393 
394     override fun onNode(
395         matcher: SemanticsMatcher,
396         useUnmergedTree: Boolean
397     ): SemanticsNodeInteraction = composeTest.onNode(matcher, useUnmergedTree)
398 
399     override fun onAllNodes(
400         matcher: SemanticsMatcher,
401         useUnmergedTree: Boolean
402     ): SemanticsNodeInteractionCollection = composeTest.onAllNodes(matcher, useUnmergedTree)
403 
404     override fun setContent(composable: @Composable () -> Unit) = composeTest.setContent(composable)
405 
406     /**
407      * Cancels AndroidComposeUiTestEnvironment's current Recomposer and creates a new one.
408      *
409      * Recreates the CoroutineContext associated with Compose being cancelled. This happens when an
410      * app moves from a regular ("Full screen") view of the app to a "Pop up" view AND certain
411      * properties in the manifest's android:configChanges are set to prevent a full tear down of the
412      * app. This is a somewhat rare case (see [AndroidComposeUiTestEnvironment] for more details).
413      */
414     fun cancelAndRecreateRecomposer() {
415         environment.cancelAndRecreateRecomposer()
416     }
417 }
418 
getActivityFromTestRulenull419 private fun <A : ComponentActivity> getActivityFromTestRule(rule: ActivityScenarioRule<A>): A {
420     var activity: A? = null
421     rule.scenario.onActivity { activity = it }
422     if (activity == null) {
423         throw IllegalStateException("Activity was not set in the ActivityScenarioRule!")
424     }
425     return activity!!
426 }
427