1 /* 2 * Copyright 2021 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.tooling 18 19 import android.app.Activity 20 import android.os.Build 21 import android.os.Bundle 22 import androidx.compose.ui.tooling.animation.AnimateXAsStateComposeAnimation 23 import androidx.compose.ui.tooling.animation.PreviewAnimationClock 24 import androidx.compose.ui.tooling.animation.UnsupportedComposeAnimation 25 import androidx.compose.ui.tooling.data.UiToolingDataApi 26 import androidx.compose.ui.tooling.test.R 27 import androidx.test.filters.LargeTest 28 import androidx.test.filters.MediumTest 29 import java.util.concurrent.CountDownLatch 30 import java.util.concurrent.TimeUnit 31 import java.util.concurrent.atomic.AtomicBoolean 32 import org.junit.Assert.assertArrayEquals 33 import org.junit.Assert.assertEquals 34 import org.junit.Assert.assertFalse 35 import org.junit.Assert.assertTrue 36 import org.junit.Before 37 import org.junit.Rule 38 import org.junit.Test 39 40 @MediumTest 41 @OptIn(UiToolingDataApi::class) 42 class ComposeViewAdapterTest { 43 @Suppress("DEPRECATION") 44 @get:Rule 45 val activityTestRule = 46 androidx.test.rule.ActivityTestRule<TestActivity>(TestActivity::class.java) 47 48 private lateinit var composeViewAdapter: ComposeViewAdapter 49 50 @Before setupnull51 fun setup() { 52 composeViewAdapter = activityTestRule.activity.findViewById(R.id.compose_view_adapter) 53 } 54 55 /** Asserts that the given Composable method executes correct and outputs some [ViewInfo]s. */ assertRendersCorrectlynull56 private fun assertRendersCorrectly(className: String, methodName: String): List<ViewInfo> { 57 initAndWaitForDraw(className, methodName) 58 activityTestRule.runOnUiThread { assertTrue(composeViewAdapter.viewInfos.isNotEmpty()) } 59 60 return composeViewAdapter.viewInfos 61 } 62 63 /** 64 * Initiates the given Composable method and waits for the [ComposeViewAdapter.onDraw] callback. 65 */ initAndWaitForDrawnull66 private fun initAndWaitForDraw( 67 className: String, 68 methodName: String, 69 designInfoProvidersArgument: String? = null 70 ) { 71 val committedAndDrawn = CountDownLatch(1) 72 val committed = AtomicBoolean(false) 73 activityTestRule.runOnUiThread { 74 composeViewAdapter.init( 75 className, 76 methodName, 77 debugViewInfos = true, 78 lookForDesignInfoProviders = true, 79 designInfoProvidersArgument = designInfoProvidersArgument, 80 onCommit = { committed.set(true) }, 81 onDraw = { 82 if (committed.get()) { 83 committedAndDrawn.countDown() 84 } 85 } 86 ) 87 } 88 89 // Workaround for a problem described in b/174291742 where onLayout will not be called 90 // after composition for the first test in the test suite. 91 activityTestRule.runOnUiThread { composeViewAdapter.requestLayout() } 92 93 // Wait for the first draw after the Composable has been committed. 94 committedAndDrawn.await() 95 } 96 97 @Test instantiateComposeViewAdapternull98 fun instantiateComposeViewAdapter() { 99 val viewInfos = 100 assertRendersCorrectly( 101 "androidx.compose.ui.tooling.SimpleComposablePreviewKt", 102 "SimpleComposablePreview" 103 ) 104 .flatMap { it.allChildren() + it } 105 .filter { it.fileName == "SimpleComposablePreview.kt" } 106 .toList() 107 108 activityTestRule.runOnUiThread { 109 assertTrue(viewInfos.isNotEmpty()) 110 // Verify that valid line numbers are being recorded 111 assertTrue(viewInfos.map { it.lineNumber }.all { it > 0 }) 112 // Verify that this composable has no animations 113 assertFalse(composeViewAdapter.hasAnimations()) 114 } 115 } 116 117 @Test lazyColumnnull118 fun lazyColumn() { 119 run { 120 composeViewAdapter.stitchTrees = false 121 val viewInfos = 122 assertRendersCorrectly( 123 "androidx.compose.ui.tooling.LazyColumnPreviewKt", 124 "SimpleLazyComposablePreview" 125 ) 126 127 assertEquals( 128 """ 129 |<root> 130 .|LazyColumnPreview.kt:31 131 |<root> 132 .|LazyColumnPreview.kt:31 133 |<root> 134 .|LazyColumnPreview.kt:31 135 |<root> 136 .|LazyColumnPreview.kt:31 137 ..|LazyColumnPreview.kt:31 138 ..|LazyColumnPreview.kt:31 139 """ 140 .trimIndent(), 141 viewInfos.toDebugString { it.fileName == "LazyColumnPreview.kt" }.trimIndent() 142 ) 143 } 144 145 run { 146 composeViewAdapter.stitchTrees = true 147 val viewInfos = 148 assertRendersCorrectly( 149 "androidx.compose.ui.tooling.LazyColumnPreviewKt", 150 "SimpleLazyComposablePreview" 151 ) 152 153 assertEquals(1, viewInfos.size) 154 assertEquals( 155 """ 156 |<root> 157 .|LazyColumnPreview.kt:31 158 ..|LazyColumnPreview.kt:31 159 ..|LazyColumnPreview.kt:31 160 ...|LazyColumnPreview.kt:31 161 ...|LazyColumnPreview.kt:31 162 ...|LazyColumnPreview.kt:31 163 """ 164 .trimIndent(), 165 viewInfos.toDebugString() { it.fileName == "LazyColumnPreview.kt" }.trimIndent() 166 ) 167 } 168 } 169 170 @Test complexTreeStitchLazyColumnnull171 fun complexTreeStitchLazyColumn() { 172 run { 173 composeViewAdapter.stitchTrees = true 174 val viewInfos = 175 assertRendersCorrectly( 176 "androidx.compose.ui.tooling.LazyColumnPreviewKt", 177 "ComplexLazyComposablePreview" 178 ) 179 180 assertEquals(1, viewInfos.size) 181 assertEquals( 182 """ 183 |<root> 184 .|LazyColumnPreview.kt:37 185 ..|LazyColumnPreview.kt:38 186 ..|LazyColumnPreview.kt:38 187 ...|LazyColumnPreview.kt:41 188 ...|LazyColumnPreview.kt:42 189 ...|LazyColumnPreview.kt:42 190 ....|LazyColumnPreview.kt:42 191 ....|LazyColumnPreview.kt:42 192 ....|LazyColumnPreview.kt:42 193 ....|LazyColumnPreview.kt:42 194 ....|LazyColumnPreview.kt:42 195 .....|LazyColumnPreview.kt:42 196 ....|LazyColumnPreview.kt:42 197 .....|LazyColumnPreview.kt:42 198 """ 199 .trimIndent(), 200 viewInfos.toDebugString() { it.fileName == "LazyColumnPreview.kt" }.trimIndent() 201 ) 202 } 203 } 204 205 @Test animatedVisibilityIsTrackednull206 fun animatedVisibilityIsTracked() { 207 val clock = PreviewAnimationClock() 208 209 activityTestRule.runOnUiThread { 210 composeViewAdapter.init( 211 "androidx.compose.ui.tooling.TestAnimationPreviewKt", 212 "AnimatedVisibilityPreview" 213 ) 214 composeViewAdapter.clock = clock 215 assertFalse(composeViewAdapter.hasAnimations()) 216 assertTrue(clock.animatedVisibilityClocks.isEmpty()) 217 } 218 219 waitFor(1, TimeUnit.SECONDS) { 220 // Handle the case where onLayout was called too soon. Calling requestLayout will 221 // make sure onLayout will be called again. 222 composeViewAdapter.requestLayout() 223 composeViewAdapter.hasAnimations() 224 } 225 226 activityTestRule.runOnUiThread { 227 val animation = clock.animatedVisibilityClocks.values.single().animation 228 assertEquals("My Animated Visibility", animation.label) 229 } 230 } 231 232 @Test transitionAnimatedVisibilityIsTrackedAsTransitionnull233 fun transitionAnimatedVisibilityIsTrackedAsTransition() { 234 checkAnimationsAreSubscribed( 235 "TransitionAnimatedVisibilityPreview", 236 emptyList(), 237 listOf("transition.AV") 238 ) 239 } 240 241 @Test animatedContentIsSubscribednull242 fun animatedContentIsSubscribed() { 243 checkAnimationsAreSubscribed( 244 "AnimatedContentPreview", 245 animatedContent = listOf("AnimatedContent") 246 ) 247 } 248 249 @Test animatedContentAndTransitionIsSubscribednull250 fun animatedContentAndTransitionIsSubscribed() { 251 checkAnimationsAreSubscribed( 252 "AnimatedContentAndTransitionPreview", 253 transitions = listOf("checkBoxAnim"), 254 animatedContent = listOf("AnimatedContent") 255 ) 256 } 257 258 @Test transitionAnimationsAreSubscribedToTheClocknull259 fun transitionAnimationsAreSubscribedToTheClock() { 260 checkAnimationsAreSubscribed("TransitionPreview", emptyList(), listOf("checkBoxAnim")) 261 } 262 263 @Test transitionAnimationsWithSubcompositionnull264 fun transitionAnimationsWithSubcomposition() { 265 checkAnimationsAreSubscribed( 266 "TransitionWithScaffoldPreview", 267 emptyList(), 268 listOf("checkBoxAnim") 269 ) 270 } 271 272 @Test animateXAsStateIsSubscribednull273 fun animateXAsStateIsSubscribed() { 274 checkAnimationsAreSubscribed( 275 "AnimateAsStatePreview", 276 animateXAsState = listOf("DpAnimation", "IntAnimation") 277 ) 278 } 279 280 @Test animateXAsStateIsNotSubscribednull281 fun animateXAsStateIsNotSubscribed() { 282 AnimateXAsStateComposeAnimation.testOverrideAvailability(false) 283 checkAnimationsAreSubscribed( 284 "AllAnimations", 285 unsupported = 286 listOf( 287 "animateContentSize", 288 "TargetBasedAnimation", 289 "DecayAnimation", 290 ), 291 transitions = listOf("checkBoxAnim", "Crossfade"), 292 animatedContent = listOf("AnimatedContent"), 293 animateXAsState = emptyList(), 294 infiniteTransitions = listOf("InfiniteTransition") 295 ) 296 AnimateXAsStateComposeAnimation.testOverrideAvailability(true) 297 } 298 299 @Test animateContentSizeIsNotSubscribednull300 fun animateContentSizeIsNotSubscribed() { 301 checkAnimationsAreSubscribed("AnimateContentSizePreview") 302 } 303 304 @Test animateContentSizeAndTransitionIsSubscribednull305 fun animateContentSizeAndTransitionIsSubscribed() { 306 checkAnimationsAreSubscribed( 307 "AnimateContentSizeAndTransitionPreview", 308 listOf("animateContentSize"), 309 listOf("checkBoxAnim") 310 ) 311 } 312 313 @Test crossFadeIsSubscribednull314 fun crossFadeIsSubscribed() { 315 checkAnimationsAreSubscribed("CrossFadePreview", emptyList(), listOf("Crossfade")) 316 } 317 318 @Test targetBasedAnimationIsNotSubscribednull319 fun targetBasedAnimationIsNotSubscribed() { 320 checkAnimationsAreSubscribed("TargetBasedAnimationPreview") 321 } 322 323 @Test decayAnimationIsNotSubscribednull324 fun decayAnimationIsNotSubscribed() { 325 checkAnimationsAreSubscribed("DecayAnimationPreview") 326 } 327 328 @Test infiniteTransitionIsSubscribednull329 fun infiniteTransitionIsSubscribed() { 330 checkAnimationsAreSubscribed( 331 "InfiniteTransitionPreview", 332 infiniteTransitions = listOf("InfiniteTransition") 333 ) 334 } 335 336 @Test targetBasedAndTransitionIsSubscribednull337 fun targetBasedAndTransitionIsSubscribed() { 338 checkAnimationsAreSubscribed( 339 "TargetBasedAndTransitionPreview", 340 listOf("TargetBasedAnimation"), 341 listOf("checkBoxAnim") 342 ) 343 } 344 345 @Test decayAndTransitionIsSubscribednull346 fun decayAndTransitionIsSubscribed() { 347 checkAnimationsAreSubscribed( 348 "DecayAndTransitionPreview", 349 listOf("DecayAnimation"), 350 listOf("checkBoxAnim") 351 ) 352 } 353 354 @Test infiniteAndTransitionIsSubscribednull355 fun infiniteAndTransitionIsSubscribed() { 356 checkAnimationsAreSubscribed( 357 "InfiniteAndTransitionPreview", 358 transitions = listOf("checkBoxAnim"), 359 infiniteTransitions = listOf("InfiniteTransition") 360 ) 361 } 362 363 @Test unsupportedAreNotSubscribedWhenEnumIsNotAvailablenull364 fun unsupportedAreNotSubscribedWhenEnumIsNotAvailable() { 365 UnsupportedComposeAnimation.testOverrideAvailability(false) 366 checkAnimationsAreSubscribed( 367 "AllAnimations", 368 unsupported = emptyList(), 369 transitions = listOf("checkBoxAnim", "Crossfade"), 370 animateXAsState = listOf("DpAnimation", "IntAnimation"), 371 animatedContent = listOf("AnimatedContent"), 372 infiniteTransitions = listOf("InfiniteTransition") 373 ) 374 UnsupportedComposeAnimation.testOverrideAvailability(true) 375 } 376 377 @Test animationsAreOrderednull378 fun animationsAreOrdered() { 379 checkAnimationsAreSubscribed( 380 "AnimationOrder", 381 emptyList(), 382 listOf("transitionOne", "transitionTwo", "transitionThree") 383 ) 384 } 385 386 @Test materialAnimationsAreSubscribednull387 fun materialAnimationsAreSubscribed() { 388 checkAnimationsAreSubscribed( 389 "MaterialPreview", 390 unsupported = emptyList(), 391 transitions = listOf("ToggleableState"), 392 animateXAsState = listOf("ColorAnimation", "ColorAnimation", "ColorAnimation") 393 ) 394 } 395 checkAnimationsAreSubscribednull396 private fun checkAnimationsAreSubscribed( 397 preview: String, 398 unsupported: List<String> = emptyList(), 399 transitions: List<String> = emptyList(), 400 animateXAsState: List<String> = emptyList(), 401 animatedContent: List<String> = emptyList(), 402 infiniteTransitions: List<String> = emptyList() 403 ) { 404 val clock = PreviewAnimationClock() 405 406 activityTestRule.runOnUiThread { 407 composeViewAdapter.init("androidx.compose.ui.tooling.TestAnimationPreviewKt", preview) 408 composeViewAdapter.clock = clock 409 assertFalse(composeViewAdapter.hasAnimations()) 410 assertTrue(clock.transitionClocks.isEmpty()) 411 assertTrue(clock.trackedUnsupportedAnimations.isEmpty()) 412 assertTrue(clock.animatedVisibilityClocks.isEmpty()) 413 assertTrue(clock.animatedContentClocks.isEmpty()) 414 assertTrue(clock.infiniteTransitionClocks.isEmpty()) 415 } 416 417 waitFor(5, TimeUnit.SECONDS) { 418 // Handle the case where onLayout was called too soon. Calling requestLayout will 419 // make sure onLayout will be called again. 420 composeViewAdapter.requestLayout() 421 composeViewAdapter.hasAnimations() 422 } 423 424 activityTestRule.runOnUiThread { 425 assertEquals(unsupported, clock.trackedUnsupportedAnimations.map { it.label }) 426 assertEquals(transitions, clock.transitionClocks.values.map { it.animation.label }) 427 assertEquals( 428 animateXAsState, 429 clock.animateXAsStateClocks.values.map { it.animation.label } 430 ) 431 assertEquals( 432 animatedContent, 433 clock.animatedContentClocks.values.map { it.animation.label } 434 ) 435 assertEquals( 436 infiniteTransitions, 437 clock.infiniteTransitionClocks.values.map { it.animation.label } 438 ) 439 assertEquals(0, clock.animatedVisibilityClocks.size) 440 } 441 } 442 443 @Test animationIsFoundWithoutClocknull444 fun animationIsFoundWithoutClock() { 445 findAnimationWithoutClock("AllAnimations") 446 } 447 448 @Test materialAnimationIsFoundWithoutClocknull449 fun materialAnimationIsFoundWithoutClock() { 450 findAnimationWithoutClock("MaterialPreview") 451 } 452 findAnimationWithoutClocknull453 private fun findAnimationWithoutClock(preview: String) { 454 activityTestRule.runOnUiThread { 455 composeViewAdapter.init("androidx.compose.ui.tooling.TestAnimationPreviewKt", preview) 456 assertFalse(composeViewAdapter.hasAnimations()) 457 } 458 459 waitFor(5, TimeUnit.SECONDS) { 460 // Handle the case where onLayout was called too soon. Calling requestLayout will 461 // make sure onLayout will be called again. 462 composeViewAdapter.requestLayout() 463 composeViewAdapter.hasAnimations() 464 } 465 assertTrue(composeViewAdapter.hasAnimations()) 466 } 467 468 @Test lineNumberMappingnull469 fun lineNumberMapping() { 470 val viewInfos = 471 assertRendersCorrectly( 472 "androidx.compose.ui.tooling.LineNumberPreviewKt", 473 "LineNumberPreview" 474 ) 475 .flatMap { it.allChildren() + it } 476 .filter { it.fileName == "LineNumberPreview.kt" } 477 .toList() 478 479 activityTestRule.runOnUiThread { 480 // Verify all calls, generate the correct line number information 481 assertArrayEquals( 482 arrayOf(35, 36, 37, 39, 42, 43, 44), 483 viewInfos.map { it.lineNumber }.sorted().distinct().toTypedArray() 484 ) 485 } 486 } 487 488 // @Test lineNumberLocationMappingnull489 fun lineNumberLocationMapping() { 490 val viewInfos = 491 assertRendersCorrectly( 492 "androidx.compose.ui.tooling.LineNumberPreviewKt", 493 "LineNumberPreview" 494 ) 495 .flatMap { it.allChildren() + it } 496 .filter { it.location?.let { it.sourceFile == "LineNumberPreview.kt" } == true } 497 .toList() 498 499 activityTestRule.runOnUiThread { 500 // Verify all calls, generate the correct line number information 501 val lines = viewInfos.map { it.location?.lineNumber ?: -1 }.sorted().toTypedArray() 502 assertArrayEquals(arrayOf(36, 37, 38, 40, 40, 40, 43, 44, 44, 45, 45), lines) 503 504 // Verify that all calls generate the correct offset information 505 val offsets = viewInfos.map { it.location?.offset ?: -1 }.sorted().toTypedArray() 506 assertArrayEquals( 507 arrayOf(1235, 1272, 1293, 1421, 1421, 1421, 1469, 1491, 1508, 1531, 1548), 508 offsets 509 ) 510 } 511 } 512 513 @Test instantiatePrivateComposeViewAdapternull514 fun instantiatePrivateComposeViewAdapter() { 515 assertRendersCorrectly( 516 "androidx.compose.ui.tooling.SimpleComposablePreviewKt", 517 "PrivateSimpleComposablePreview" 518 ) 519 } 520 521 @Test defaultParametersComposableTest1null522 fun defaultParametersComposableTest1() { 523 assertRendersCorrectly( 524 "androidx.compose.ui.tooling.SimpleComposablePreviewKt", 525 "DefaultParametersPreview1" 526 ) 527 } 528 529 @Test defaultParametersComposableTest2null530 fun defaultParametersComposableTest2() { 531 assertRendersCorrectly( 532 "androidx.compose.ui.tooling.SimpleComposablePreviewKt", 533 "DefaultParametersPreview2" 534 ) 535 } 536 537 @Test defaultParametersComposableTest3null538 fun defaultParametersComposableTest3() { 539 assertRendersCorrectly( 540 "androidx.compose.ui.tooling.SimpleComposablePreviewKt", 541 "DefaultParametersPreview3" 542 ) 543 } 544 545 /** 546 * Verifies the use of inline classes as preview default parameters. Methods with inline classes 547 * as parameters will get the name mangled so we need to ensure we invoke correctly the right 548 * method. 549 */ 550 @Test defaultParametersComposableTest4null551 fun defaultParametersComposableTest4() { 552 assertRendersCorrectly( 553 "androidx.compose.ui.tooling.SimpleComposablePreviewKt", 554 "DefaultParametersPreview4" 555 ) 556 } 557 558 @Test PreviewParametersComposableTestnull559 fun PreviewParametersComposableTest() { 560 assertRendersCorrectly( 561 "androidx.compose.ui.tooling.SimpleComposablePreviewKt", 562 "PreviewParametersComposablePreview" 563 ) 564 } 565 566 @Test previewInClassnull567 fun previewInClass() { 568 assertRendersCorrectly("androidx.compose.ui.tooling.TestGroup", "InClassPreview") 569 } 570 571 @Test lifecycleUsedInsidePreviewnull572 fun lifecycleUsedInsidePreview() { 573 assertRendersCorrectly( 574 "androidx.compose.ui.tooling.SimpleComposablePreviewKt", 575 "LifecyclePreview" 576 ) 577 } 578 579 @Test saveableStateRegistryUsedInsidePreviewnull580 fun saveableStateRegistryUsedInsidePreview() { 581 assertRendersCorrectly( 582 "androidx.compose.ui.tooling.SimpleComposablePreviewKt", 583 "SaveableStateRegistryPreview" 584 ) 585 } 586 587 @Test onBackPressedDispatcherUsedInsidePreviewnull588 fun onBackPressedDispatcherUsedInsidePreview() { 589 assertRendersCorrectly( 590 "androidx.compose.ui.tooling.SimpleComposablePreviewKt", 591 "OnBackPressedDispatcherPreview" 592 ) 593 } 594 595 @Test activityResultRegistryUsedInsidePreviewnull596 fun activityResultRegistryUsedInsidePreview() { 597 assertRendersCorrectly( 598 "androidx.compose.ui.tooling.SimpleComposablePreviewKt", 599 "ActivityResultRegistryPreview" 600 ) 601 } 602 603 @Test viewModelPreviewRendersCorrectlynull604 fun viewModelPreviewRendersCorrectly() { 605 assertRendersCorrectly( 606 "androidx.compose.ui.tooling.SimpleComposablePreviewKt", 607 "ViewModelPreview" 608 ) 609 } 610 611 @Test multipreviewTestnull612 fun multipreviewTest() { 613 assertRendersCorrectly( 614 "androidx.compose.ui.tooling.SimpleComposablePreviewKt", 615 "Multipreview" 616 ) 617 assertRendersCorrectly( 618 "androidx.compose.ui.tooling.SimpleComposablePreviewKt", 619 "MultiPreviews" 620 ) 621 } 622 623 /** Check that no re-composition happens without forcing it. */ 624 @LargeTest 625 @Test testNoInvalidationnull626 fun testNoInvalidation() { 627 compositionCount.set(0) 628 var onDrawCounter = 0 629 activityTestRule.runOnUiThread { 630 composeViewAdapter.init( 631 "androidx.compose.ui.tooling.TestInvalidationPreviewKt", 632 "CounterPreview", 633 onDraw = { onDrawCounter++ } 634 ) 635 } 636 637 // API before 29, might issue an additional draw under testing. 638 val expectedDrawCount = if (Build.VERSION.SDK_INT < 29) 2 else 1 639 repeat(5) { 640 activityTestRule.runOnUiThread { 641 assertEquals(1, compositionCount.get()) 642 assertTrue( 643 "At most, $expectedDrawCount draw is expected ($onDrawCounter happened)", 644 onDrawCounter <= expectedDrawCount 645 ) 646 } 647 Thread.sleep(250) 648 } 649 } 650 651 @Test simpleDesignInfoProviderTestnull652 fun simpleDesignInfoProviderTest() { 653 checkDesignInfoList("DesignInfoProviderA", "A", "ObjectA, x=0, y=0") 654 checkDesignInfoList("DesignInfoProviderA", "B", "Invalid, x=0, y=0") 655 656 checkDesignInfoList("DesignInfoProviderB", "A", "Invalid, x=0, y=0") 657 checkDesignInfoList("DesignInfoProviderB", "B", "ObjectB, x=0, y=0") 658 } 659 660 @Test subcompositionDesignInfoProviderTestnull661 fun subcompositionDesignInfoProviderTest() { 662 checkDesignInfoList("ScaffoldDesignInfoProvider", "A", "ObjectA, x=0, y=0") 663 } 664 checkDesignInfoListnull665 private fun checkDesignInfoList( 666 methodName: String, 667 customArgument: String, 668 expectedResult: String 669 ) { 670 initAndWaitForDraw( 671 "androidx.compose.ui.tooling.DesignInfoProviderComposableKt", 672 methodName, 673 customArgument 674 ) 675 676 activityTestRule.runOnUiThread { 677 assertTrue(composeViewAdapter.designInfoList.isNotEmpty()) 678 } 679 680 assertEquals(1, composeViewAdapter.designInfoList.size) 681 assertEquals(expectedResult, composeViewAdapter.designInfoList[0]) 682 } 683 684 /** 685 * Waits for a given condition to be satisfied within a given timeout. Fails the test when 686 * timing out. The condition is evaluated on the UI thread. 687 */ waitFornull688 private fun waitFor(timeout: Long, timeUnit: TimeUnit, conditionExpression: () -> Boolean) { 689 val conditionSatisfied = AtomicBoolean(false) 690 val now = System.nanoTime() 691 val timeoutNanos = timeUnit.toNanos(timeout) 692 while (!conditionSatisfied.get()) { 693 activityTestRule.runOnUiThread { conditionSatisfied.set(conditionExpression()) } 694 if ((System.nanoTime() - now) > timeoutNanos) { 695 // Some previews are expected not to have animations. 696 return 697 } 698 Thread.sleep(200) 699 } 700 } 701 702 companion object { 703 class TestActivity : Activity() { onCreatenull704 override fun onCreate(savedInstanceState: Bundle?) { 705 super.onCreate(savedInstanceState) 706 setContentView(R.layout.compose_adapter_test) 707 } 708 } 709 } 710 } 711