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