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