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 @file:Suppress("PLUGIN_ERROR")
18
19 package androidx.compose.runtime
20
21 import android.app.Activity
22 import android.os.Looper
23 import android.view.Choreographer
24 import android.view.View
25 import android.view.ViewGroup
26 import androidx.activity.ComponentActivity
27 import androidx.activity.compose.setContent
28 import androidx.compose.runtime.snapshots.Snapshot
29 import androidx.compose.ui.platform.LocalContext
30 import java.util.concurrent.CountDownLatch
31 import java.util.concurrent.TimeUnit
32 import kotlin.test.assertTrue
33
34 class TestActivity : ComponentActivity()
35
36 @Suppress("DEPRECATION")
makeTestActivityRulenull37 fun makeTestActivityRule() = androidx.test.rule.ActivityTestRule(TestActivity::class.java)
38
39 internal val Activity.root
40 get() = findViewById<ViewGroup>(android.R.id.content)
41
42 internal fun Activity.uiThread(block: () -> Unit) {
43 val latch = CountDownLatch(1)
44 var throwable: Throwable? = null
45 runOnUiThread(
46 object : Runnable {
47 override fun run() {
48 try {
49 block()
50 } catch (e: Throwable) {
51 throwable = e
52 } finally {
53 latch.countDown()
54 }
55 }
56 }
57 )
58
59 val completed = latch.await(5, TimeUnit.SECONDS)
60 if (!completed) error("UI thread work did not complete within 5 seconds")
61 throwable?.let {
62 throw when (it) {
63 is AssertionError -> AssertionError(it.localizedMessage, it)
64 else ->
65 IllegalStateException("UI thread threw an exception: ${it.localizedMessage}", it)
66 }
67 }
68 }
69
70 internal fun ComponentActivity.show(block: @Composable () -> Unit) {
<lambda>null71 uiThread {
72 Snapshot.sendApplyNotifications()
73 setContent(content = block)
74 }
75 }
76
waitForAFramenull77 internal fun Activity.waitForAFrame() {
78 if (Looper.getMainLooper() == Looper.myLooper()) {
79 throw Exception("Cannot be run from the main thread")
80 }
81 val latch = CountDownLatch(1)
82 uiThread { Choreographer.getInstance().postFrameCallback { latch.countDown() } }
83 assertTrue(latch.await(1, TimeUnit.MINUTES), "Time-out waiting for choreographer frame")
84 }
85
86 abstract class BaseComposeTest {
87
88 @Suppress("DEPRECATION")
89 abstract val activityRule: androidx.test.rule.ActivityTestRule<TestActivity>
90
91 val activity
92 get() = activityRule.activity
93
composenull94 fun compose(composable: @Composable () -> Unit) = ComposeTester(activity, composable)
95
96 @Composable
97 @Suppress("UNUSED_PARAMETER")
98 fun subCompose(block: @Composable () -> Unit) {
99 // val reference = rememberCompositionContext()
100 // remember {
101 // Composition(
102 // UiApplier(View(activity)),
103 // reference
104 // )
105 // }.apply {
106 // setContent {
107 // block()
108 // }
109 // }
110 }
111 }
112
113 class ComposeTester(val activity: ComponentActivity, val composable: @Composable () -> Unit) {
114 inner class ActiveTest(val activity: Activity) {
thennull115 fun then(block: ActiveTest.(activity: Activity) -> Unit): ActiveTest {
116 activity.waitForAFrame()
117 activity.uiThread { block(activity) }
118 return this
119 }
120
donenull121 fun done() {
122 activity.uiThread { activity.setContentView(View(activity)) }
123 activity.waitForAFrame()
124 }
125 }
126
initialCompositionnull127 private fun initialComposition(composable: @Composable () -> Unit) {
128 activity.show { CompositionLocalProvider(LocalContext provides activity) { composable() } }
129 }
130
thennull131 fun then(block: ComposeTester.(activity: Activity) -> Unit): ActiveTest {
132 initialComposition(composable)
133 activity.waitForAFrame()
134 activity.uiThread { block(activity) }
135 return ActiveTest(activity)
136 }
137 }
138
<lambda>null139 fun View.traversal(): Sequence<View> = sequence {
140 yield(this@traversal)
141 if (this@traversal is ViewGroup) {
142 for (i in 0 until childCount) {
143 yieldAll(getChildAt(i).traversal())
144 }
145 }
146 }
147