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