1 /*
2  * Copyright 2019 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.testutils
18 
19 import androidx.activity.ComponentActivity
20 import androidx.compose.foundation.clickable
21 import androidx.compose.foundation.layout.Box
22 import androidx.compose.foundation.layout.fillMaxSize
23 import androidx.compose.material.Text
24 import androidx.compose.runtime.Composable
25 import androidx.compose.runtime.LaunchedEffect
26 import androidx.compose.runtime.SideEffect
27 import androidx.compose.runtime.mutableStateOf
28 import androidx.compose.runtime.remember
29 import androidx.compose.runtime.rememberCoroutineScope
30 import androidx.compose.ui.Modifier
31 import androidx.compose.ui.focus.FocusRequester
32 import androidx.compose.ui.focus.FocusState
33 import androidx.compose.ui.focus.focusRequester
34 import androidx.compose.ui.focus.focusTarget
35 import androidx.compose.ui.focus.onFocusChanged
36 import androidx.compose.ui.node.ModifierNodeElement
37 import androidx.compose.ui.test.junit4.AndroidComposeTestRule
38 import androidx.compose.ui.test.junit4.createAndroidComposeRule
39 import androidx.test.ext.junit.rules.ActivityScenarioRule
40 import androidx.test.ext.junit.runners.AndroidJUnit4
41 import androidx.test.filters.MediumTest
42 import com.google.common.truth.Truth.assertThat
43 import kotlin.coroutines.suspendCoroutine
44 import kotlinx.coroutines.delay
45 import kotlinx.coroutines.launch
46 import kotlinx.coroutines.suspendCancellableCoroutine
47 import kotlinx.coroutines.yield
48 import org.junit.Rule
49 import org.junit.Test
50 import org.junit.runner.RunWith
51 
52 @MediumTest
53 @RunWith(AndroidJUnit4::class)
54 class AndroidComposeTestCaseRunnerTest {
55 
56     @get:Rule val composeTestRule = createAndroidComposeRule<ComponentActivity>()
57 
58     internal fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>
59         .forGivenContent(composable: @Composable () -> Unit): ComposeTestCaseSetup {
60         return forGivenTestCase(
61             object : ComposeTestCase {
62                 @Composable
Contentnull63                 override fun Content() {
64                     composable()
65                 }
66             }
67         )
68     }
69 
70     @Test
foreverRecomposing_viaModel_shouldFailnull71     fun foreverRecomposing_viaModel_shouldFail() {
72         val count = mutableStateOf(0)
73         composeTestRule
74             .forGivenContent {
75                 Text("Hello ${count.value}")
76                 count.value++
77             }
78             .performTestWithEventsControl {
79                 // Force the first recompose as the changes during initial composition are not
80                 // considered to invalidate the composition.
81                 count.value++
82                 assertFailsWith<AssertionError>("Changes are still pending after '10' frames.") {
83                     doFramesAssertAllHadChangesExceptLastOne(10)
84                 }
85             }
86     }
87 
88     // @Test //- TODO: Does not work, performs only 1 frame until stable
foreverRecomposing_viaState_shouldFailnull89     fun foreverRecomposing_viaState_shouldFail() {
90         composeTestRule
91             .forGivenContent {
92                 val state = remember { mutableStateOf(0) }
93                 Text("Hello ${state.value}")
94                 state.value++
95             }
96             .performTestWithEventsControl {
97                 assertFailsWith<AssertionError>("Changes are still pending after '10' frames.") {
98                     doFramesAssertAllHadChangesExceptLastOne(10)
99                 }
100             }
101     }
102 
103     // @Test //- TODO: Does not work, performs only 1 frame until stable
foreverRecomposing_viaStatePreCommit_shouldFailnull104     fun foreverRecomposing_viaStatePreCommit_shouldFail() {
105         composeTestRule
106             .forGivenContent {
107                 val state = remember { mutableStateOf(0) }
108                 Text("Hello ${state.value}")
109                 SideEffect { state.value++ }
110             }
111             .performTestWithEventsControl {
112                 assertFailsWith<AssertionError>("Changes are still pending after '10' frames.") {
113                     doFramesAssertAllHadChangesExceptLastOne(10)
114                 }
115             }
116     }
117 
118     @Test
recomposeZeroTimenull119     fun recomposeZeroTime() {
120         composeTestRule
121             .forGivenContent {
122                 // Just empty composable
123             }
124             .performTestWithEventsControl {
125                 doFrame()
126                 assertNoPendingChanges()
127             }
128     }
129 
130     @Test
recomposeZeroTime2null131     fun recomposeZeroTime2() {
132         composeTestRule
133             .forGivenContent { Text("Hello") }
134             .performTestWithEventsControl {
135                 doFrame()
136                 assertNoPendingChanges()
137             }
138     }
139 
140     @Test
recomposeOncenull141     fun recomposeOnce() {
142         composeTestRule
143             .forGivenContent {
144                 val state = remember { mutableStateOf(0) }
145                 if (state.value < 1) {
146                     state.value++
147                 }
148             }
149             .performTestWithEventsControl {
150                 doFrame()
151                 assertNoPendingChanges()
152             }
153     }
154 
155     // @Test //- TODO: Does not work, performs only 1 frame until stable
recomposeTwicenull156     fun recomposeTwice() {
157         composeTestRule
158             .forGivenContent {
159                 val state = remember { mutableStateOf(0) }
160                 if (state.value < 2) {
161                     state.value++
162                 }
163             }
164             .performTestWithEventsControl { doFramesAssertAllHadChangesExceptLastOne(2) }
165     }
166 
167     @Test
recomposeTwice2null168     fun recomposeTwice2() {
169         val count = mutableStateOf(0)
170         composeTestRule
171             .forGivenContent {
172                 Text("Hello ${count.value}")
173                 if (count.value < 3) {
174                     count.value++
175                 }
176             }
177             .performTestWithEventsControl {
178                 // Force the first recompose as the changes during initial composition are not
179                 // considered to invalidate the composition.
180                 count.value++
181                 doFramesAssertAllHadChangesExceptLastOne(2)
182             }
183     }
184 
185     @Test
measurePositiveOnEmptyShouldFailnull186     fun measurePositiveOnEmptyShouldFail() {
187         composeTestRule
188             .forGivenContent {
189                 // Just empty composable
190             }
191             .performTestWithEventsControl {
192                 doFrame()
193                 assertFailsWith<AssertionError> { assertMeasureSizeIsPositive() }
194             }
195     }
196 
197     @Test
measurePositivenull198     fun measurePositive() {
199         composeTestRule
200             .forGivenContent { Box { Text("Hello") } }
201             .performTestWithEventsControl {
202                 doFrame()
203                 assertMeasureSizeIsPositive()
204             }
205     }
206 
207     @Test
layout_preservesActiveFocusnull208     fun layout_preservesActiveFocus() {
209         lateinit var focusState: FocusState
210         composeTestRule
211             .forGivenContent {
212                 val focusRequester = remember { FocusRequester() }
213                 Box(
214                     Modifier.fillMaxSize()
215                         .onFocusChanged { focusState = it }
216                         .focusRequester(focusRequester)
217                         .focusTarget()
218                 )
219                 LaunchedEffect(Unit) { focusRequester.requestFocus() }
220             }
221             .performTestWithEventsControl {
222                 doFrame()
223                 assertThat(focusState.isFocused).isTrue()
224             }
225     }
226 
227     @Test
countLaunchedCoroutines_noContentLaunchesnull228     fun countLaunchedCoroutines_noContentLaunches() {
229         composeTestRule
230             .forGivenContent { Box { Text("Hello") } }
231             .performTestWithEventsControl { assertCoroutinesCount(0) }
232     }
233 
234     @Test
countLaunchedCoroutines_modifierLaunchesnull235     fun countLaunchedCoroutines_modifierLaunches() {
236         val node =
237             object : Modifier.Node() {
238                 override fun onAttach() {
239                     super.onAttach()
240                     coroutineScope.launch {}
241                 }
242             }
243         val element =
244             object : ModifierNodeElement<Modifier.Node>() {
245                 override fun create(): Modifier.Node = node
246 
247                 override fun update(node: Modifier.Node) {
248                     // no op
249                 }
250 
251                 override fun hashCode(): Int = 0
252 
253                 override fun equals(other: Any?): Boolean = false
254             }
255         composeTestRule
256             .forGivenContent { Box(Modifier.then(element)) { Text("Hello") } }
257             .performTestWithEventsControl { assertCoroutinesCount(1) }
258     }
259 
260     @Test
countLaunchedCoroutines_launchedEffectnull261     fun countLaunchedCoroutines_launchedEffect() {
262         composeTestRule
263             .forGivenContent { LaunchedEffect(Unit) { launch {} } }
264             .performTestWithEventsControl { assertCoroutinesCount(2) }
265     }
266 
267     @Test
countLaunchedCoroutines_scopeLaunches_lazynull268     fun countLaunchedCoroutines_scopeLaunches_lazy() {
269         composeTestRule
270             .forGivenContent {
271                 val scope = rememberCoroutineScope()
272                 Box(Modifier.clickable { scope.launch {} }) { Text("Hello") }
273             }
274             .performTestWithEventsControl { assertCoroutinesCount(0) }
275     }
276 
277     @Test
countLaunchedCoroutines_suspendnull278     fun countLaunchedCoroutines_suspend() {
279         composeTestRule
280             .forGivenContent {
281                 LaunchedEffect(Unit) { suspendCancellableCoroutine {} }
282 
283                 LaunchedEffect(Unit) { suspendCoroutine {} }
284             }
285             .performTestWithEventsControl { assertCoroutinesCount(2) }
286     }
287 
288     @Test
countLaunchedCoroutines_delaynull289     fun countLaunchedCoroutines_delay() {
290         composeTestRule
291             .forGivenContent {
292                 LaunchedEffect(Unit) { delay(1_000L) }
293 
294                 LaunchedEffect(Unit) { launch {} }
295             }
296             .performTestWithEventsControl { assertCoroutinesCount(3) }
297     }
298 
299     @Test
countLaunchedCoroutines_yieldnull300     fun countLaunchedCoroutines_yield() {
301         composeTestRule
302             .forGivenContent {
303                 LaunchedEffect(Unit) { yield() }
304 
305                 LaunchedEffect(Unit) { launch {} }
306             }
307             .performTestWithEventsControl { assertCoroutinesCount(3) }
308     }
309 
assertFailsWithnull310     private inline fun <reified T : Throwable> assertFailsWith(
311         expectedErrorMessage: String? = null,
312         block: () -> Any
313     ) {
314         try {
315             block()
316         } catch (e: Throwable) {
317             if (e !is T) {
318                 throw AssertionError("Expected exception not thrown, received: $e")
319             }
320             if (expectedErrorMessage != null && e.localizedMessage != expectedErrorMessage) {
321                 throw AssertionError(
322                     "Expected error message not found, received: '" + "${e.localizedMessage}'"
323                 )
324             }
325             return
326         }
327 
328         throw AssertionError("Expected exception not thrown")
329     }
330 }
331