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