1 /*
<lambda>null2  * 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 package androidx.constraintlayout.compose
18 
19 import android.content.Context
20 import android.os.Build
21 import androidx.compose.animation.core.tween
22 import androidx.compose.foundation.background
23 import androidx.compose.foundation.layout.Box
24 import androidx.compose.foundation.layout.Column
25 import androidx.compose.foundation.layout.IntrinsicSize
26 import androidx.compose.foundation.layout.aspectRatio
27 import androidx.compose.foundation.layout.fillMaxSize
28 import androidx.compose.foundation.layout.fillMaxWidth
29 import androidx.compose.foundation.layout.height
30 import androidx.compose.foundation.layout.padding
31 import androidx.compose.foundation.layout.size
32 import androidx.compose.foundation.layout.width
33 import androidx.compose.foundation.layout.wrapContentHeight
34 import androidx.compose.foundation.rememberScrollState
35 import androidx.compose.foundation.verticalScroll
36 import androidx.compose.runtime.CompositionLocalProvider
37 import androidx.compose.runtime.getValue
38 import androidx.compose.runtime.mutableStateOf
39 import androidx.compose.runtime.remember
40 import androidx.compose.runtime.setValue
41 import androidx.compose.ui.Modifier
42 import androidx.compose.ui.geometry.Offset
43 import androidx.compose.ui.graphics.Color
44 import androidx.compose.ui.graphics.asAndroidBitmap
45 import androidx.compose.ui.graphics.toArgb
46 import androidx.compose.ui.layout.FirstBaseline
47 import androidx.compose.ui.layout.LookaheadScope
48 import androidx.compose.ui.layout.boundsInParent
49 import androidx.compose.ui.layout.layout
50 import androidx.compose.ui.layout.layoutId
51 import androidx.compose.ui.layout.onGloballyPositioned
52 import androidx.compose.ui.layout.onPlaced
53 import androidx.compose.ui.layout.positionInParent
54 import androidx.compose.ui.layout.positionInRoot
55 import androidx.compose.ui.node.Ref
56 import androidx.compose.ui.platform.InspectableValue
57 import androidx.compose.ui.platform.LocalLayoutDirection
58 import androidx.compose.ui.platform.ValueElement
59 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
60 import androidx.compose.ui.platform.testTag
61 import androidx.compose.ui.test.assertHeightIsEqualTo
62 import androidx.compose.ui.test.assertPositionInRootIsEqualTo
63 import androidx.compose.ui.test.assertWidthIsEqualTo
64 import androidx.compose.ui.test.captureToImage
65 import androidx.compose.ui.test.junit4.createComposeRule
66 import androidx.compose.ui.test.onNodeWithTag
67 import androidx.compose.ui.unit.Constraints
68 import androidx.compose.ui.unit.Dp
69 import androidx.compose.ui.unit.IntOffset
70 import androidx.compose.ui.unit.IntSize
71 import androidx.compose.ui.unit.LayoutDirection
72 import androidx.compose.ui.unit.dp
73 import androidx.compose.ui.unit.round
74 import androidx.compose.ui.util.fastRoundToInt
75 import androidx.test.core.app.ApplicationProvider
76 import androidx.test.ext.junit.runners.AndroidJUnit4
77 import androidx.test.filters.MediumTest
78 import androidx.test.filters.SdkSuppress
79 import kotlin.math.roundToInt
80 import kotlin.test.assertNotEquals
81 import kotlin.test.assertTrue
82 import org.junit.After
83 import org.junit.Assert.assertEquals
84 import org.junit.Assert.assertNull
85 import org.junit.Before
86 import org.junit.Ignore
87 import org.junit.Rule
88 import org.junit.Test
89 import org.junit.runner.RunWith
90 
91 @MediumTest
92 @RunWith(AndroidJUnit4::class)
93 class ConstraintLayoutTest {
94     @get:Rule val rule = createComposeRule()
95 
96     private var displaySize: IntSize = IntSize.Zero
97 
98     // region sizing tests
99 
100     @Before
101     fun before() {
102         isDebugInspectorInfoEnabled = true
103         displaySize =
104             ApplicationProvider.getApplicationContext<Context>().resources.displayMetrics.let {
105                 IntSize(it.widthPixels, it.heightPixels)
106             }
107     }
108 
109     @After
110     fun after() {
111         isDebugInspectorInfoEnabled = false
112     }
113 
114     @Test
115     fun dividerMatchTextHeight_spread() =
116         with(rule.density) {
117             val aspectRatioBoxSize = Ref<IntSize>()
118             val dividerSize = Ref<IntSize>()
119 
120             rule.setContent {
121                 ConstraintLayout(
122                     // Make CL fixed width and wrap content height.
123                     modifier = Modifier.fillMaxWidth()
124                 ) {
125                     val (aspectRatioBox, divider) = createRefs()
126                     val guideline = createGuidelineFromAbsoluteLeft(0.5f)
127 
128                     Box(
129                         Modifier.constrainAs(aspectRatioBox) {
130                                 centerTo(parent)
131                                 start.linkTo(guideline)
132                                 width = Dimension.preferredWrapContent
133                                 height = Dimension.wrapContent
134                             }
135                             // Try to be large to make wrap content impossible.
136                             .width((displaySize.width).toDp())
137                             // This could be any (width in height out child) e.g. text
138                             .aspectRatio(2f)
139                             .onGloballyPositioned { coordinates ->
140                                 aspectRatioBoxSize.value = coordinates.size
141                             }
142                     )
143                     Box(
144                         Modifier.constrainAs(divider) {
145                                 centerTo(parent)
146                                 width = Dimension.value(1.dp)
147                                 height = Dimension.fillToConstraints
148                             }
149                             .onGloballyPositioned { coordinates ->
150                                 dividerSize.value = coordinates.size
151                             }
152                     )
153                 }
154             }
155 
156             rule.runOnIdle {
157                 // The aspect ratio could not wrap and it is wrap suggested, so it respects
158                 // constraints.
159                 assertEquals(
160                     (displaySize.width / 2f).roundToInt(),
161                     aspectRatioBoxSize.value!!.width
162                 )
163                 // Aspect ratio is preserved.
164                 assertEquals(
165                     (displaySize.width / 2f / 2f).roundToInt(),
166                     aspectRatioBoxSize.value!!.height
167                 )
168                 // Divider has fixed width 1.dp in constraint set.
169                 assertEquals(1.dp.roundToPx(), dividerSize.value!!.width)
170                 // Divider has spread height so it should spread to fill the height of the CL,
171                 // which in turns is given by the size of the aspect ratio box.
172                 assertEquals(aspectRatioBoxSize.value!!.height, dividerSize.value!!.height)
173             }
174         }
175 
176     @Test
177     fun dividerMatchTextHeight_spread_withPreferredWrapHeightText() =
178         with(rule.density) {
179             val aspectRatioBoxSize = Ref<IntSize>()
180             val dividerSize = Ref<IntSize>()
181             rule.setContent {
182                 ConstraintLayout(
183                     // Make CL fixed width and wrap content height.
184                     modifier = Modifier.fillMaxWidth()
185                 ) {
186                     val (aspectRatioBox, divider) = createRefs()
187                     val guideline = createGuidelineFromAbsoluteLeft(0.5f)
188 
189                     Box(
190                         Modifier.constrainAs(aspectRatioBox) {
191                                 centerTo(parent)
192                                 start.linkTo(guideline)
193                                 width = Dimension.preferredWrapContent
194                                 height = Dimension.preferredWrapContent
195                             }
196                             // Try to be large to make wrap content impossible.
197                             .width((displaySize.width).toDp())
198                             // This could be any (width in height out child) e.g. text
199                             .aspectRatio(2f)
200                             .onGloballyPositioned { coordinates ->
201                                 aspectRatioBoxSize.value = coordinates.size
202                             }
203                     )
204                     Box(
205                         Modifier.constrainAs(divider) {
206                                 centerTo(parent)
207                                 width = Dimension.value(1.dp)
208                                 height = Dimension.fillToConstraints
209                             }
210                             .onGloballyPositioned { coordinates ->
211                                 dividerSize.value = coordinates.size
212                             }
213                     )
214                 }
215             }
216 
217             rule.runOnIdle {
218                 // The aspect ratio could not wrap and it is wrap suggested, so it respects
219                 // constraints.
220                 assertEquals(
221                     (displaySize.width / 2f).roundToInt(),
222                     aspectRatioBoxSize.value!!.width
223                 )
224                 // Aspect ratio is preserved.
225                 assertEquals(
226                     (displaySize.width / 2f / 2f).roundToInt(),
227                     aspectRatioBoxSize.value!!.height
228                 )
229                 // Divider has fixed width 1.dp in constraint set.
230                 assertEquals(1.dp.roundToPx(), dividerSize.value!!.width)
231                 // Divider has spread height so it should spread to fill the height of the CL,
232                 // which in turns is given by the size of the aspect ratio box.
233                 assertEquals(aspectRatioBoxSize.value!!.height, dividerSize.value!!.height)
234             }
235         }
236 
237     @Test
238     fun dividerMatchTextHeight_percent() =
239         with(rule.density) {
240             val aspectRatioBoxSize = Ref<IntSize>()
241             val dividerSize = Ref<IntSize>()
242             rule.setContent {
243                 ConstraintLayout(
244                     // Make CL fixed width and wrap content height.
245                     modifier = Modifier.fillMaxWidth()
246                 ) {
247                     val (aspectRatioBox, divider) = createRefs()
248                     val guideline = createGuidelineFromAbsoluteLeft(0.5f)
249 
250                     Box(
251                         Modifier.constrainAs(aspectRatioBox) {
252                                 centerTo(parent)
253                                 start.linkTo(guideline)
254                                 width = Dimension.preferredWrapContent
255                                 height = Dimension.wrapContent
256                             }
257                             // Try to be large to make wrap content impossible.
258                             .width((displaySize.width).toDp())
259                             // This could be any (width in height out child) e.g. text
260                             .aspectRatio(2f)
261                             .onGloballyPositioned { coordinates ->
262                                 aspectRatioBoxSize.value = coordinates.size
263                             }
264                     )
265                     Box(
266                         Modifier.constrainAs(divider) {
267                                 centerTo(parent)
268                                 width = Dimension.value(1.dp)
269                                 height = Dimension.percent(0.8f)
270                             }
271                             .onGloballyPositioned { coordinates ->
272                                 dividerSize.value = coordinates.size
273                             }
274                     )
275                 }
276             }
277 
278             rule.runOnIdle {
279                 // The aspect ratio could not wrap and it is wrap suggested, so it respects
280                 // constraints.
281                 assertEquals(
282                     (displaySize.width / 2f).roundToInt(),
283                     aspectRatioBoxSize.value!!.width
284                 )
285                 // Aspect ratio is preserved.
286                 assertEquals(
287                     (displaySize.width / 2f / 2f).roundToInt(),
288                     aspectRatioBoxSize.value!!.height
289                 )
290                 // Divider has fixed width 1.dp in constraint set.
291                 assertEquals(1.dp.roundToPx(), dividerSize.value!!.width)
292                 // Divider has percent height so it should spread to fill 0.8 of the height of the
293                 // CL,
294                 // which in turns is given by the size of the aspect ratio box.
295                 assertEquals(
296                     (aspectRatioBoxSize.value!!.height * 0.8f).roundToInt(),
297                     dividerSize.value!!.height
298                 )
299             }
300         }
301 
302     @Test
303     @Ignore
304     fun dividerMatchTextHeight_inWrapConstraintLayout_longText() =
305         with(rule.density) {
306             val aspectRatioBoxSize = Ref<IntSize>()
307             val dividerSize = Ref<IntSize>()
308             rule.setContent {
309                 // CL is wrap content.
310                 ConstraintLayout {
311                     val (aspectRatioBox, divider) = createRefs()
312                     val guideline = createGuidelineFromAbsoluteLeft(0.5f)
313 
314                     Box(
315                         Modifier.constrainAs(aspectRatioBox) {
316                                 centerTo(parent)
317                                 start.linkTo(guideline)
318                                 width = Dimension.preferredWrapContent
319                                 height = Dimension.wrapContent
320                             }
321                             // Try to be large to make wrap content impossible.
322                             .width((displaySize.width).toDp())
323                             // This could be any (width in height out child) e.g. text
324                             .aspectRatio(2f)
325                             .onGloballyPositioned { coordinates ->
326                                 aspectRatioBoxSize.value = coordinates.size
327                             }
328                     )
329                     Box(
330                         Modifier.constrainAs(divider) {
331                                 centerTo(parent)
332                                 width = Dimension.value(1.dp)
333                                 height = Dimension.percent(0.8f)
334                             }
335                             .onGloballyPositioned { coordinates ->
336                                 dividerSize.value = coordinates.size
337                             }
338                     )
339                 }
340             }
341 
342             rule.runOnIdle {
343                 // The aspect ratio could not wrap and it is wrap suggested, so it respects
344                 // constraints.
345                 assertEquals((displaySize.width / 2), aspectRatioBoxSize.value!!.width)
346                 // Aspect ratio is preserved.
347                 assertEquals((displaySize.width / 2 / 2), aspectRatioBoxSize.value!!.height)
348                 // Divider has fixed width 1.dp in constraint set.
349                 assertEquals(1.dp.roundToPx(), dividerSize.value!!.width)
350                 // Divider has percent height so it should spread to fill 0.8 of the height of the
351                 // CL,
352                 // which in turns is given by the size of the aspect ratio box.
353                 // TODO(popam; b/150277566): uncomment
354                 assertEquals(
355                     "broken, display size ${displaySize.width}x${displaySize.height} aspect" +
356                         " height ${aspectRatioBoxSize.value!!.width}x" +
357                         "${aspectRatioBoxSize.value!!.height}, " +
358                         "divider: ${dividerSize.value!!.height}",
359                     (aspectRatioBoxSize.value!!.height * 0.8f).roundToInt(),
360                     dividerSize.value!!.height
361                 )
362                 assertEquals(
363                     "broken, aspect height ${aspectRatioBoxSize.value!!.width}x" +
364                         "${aspectRatioBoxSize.value!!.height}," +
365                         " divider: ${dividerSize.value!!.height}",
366                     aspectRatioBoxSize.value!!.width,
367                     540
368                 )
369             }
370         }
371 
372     @Test
373     fun dividerMatchTextHeight_inWrapConstraintLayout_shortText() =
374         with(rule.density) {
375             val constraintLayoutSize = Ref<IntSize>()
376             val aspectRatioBoxSize = Ref<IntSize>()
377             val dividerSize = Ref<IntSize>()
378             val size = 40.toDp()
379             rule.setContent {
380                 ConstraintLayout(
381                     // CL is wrapping width and height.
382                     modifier =
383                         Modifier.onGloballyPositioned { constraintLayoutSize.value = it.size }
384                 ) {
385                     val (aspectRatioBox, divider) = createRefs()
386                     val guideline = createGuidelineFromAbsoluteLeft(0.5f)
387 
388                     Box(
389                         Modifier.constrainAs(aspectRatioBox) {
390                                 centerTo(parent)
391                                 start.linkTo(guideline)
392                                 width = Dimension.preferredWrapContent
393                                 height = Dimension.wrapContent
394                             }
395                             // Small width for the CL to wrap it.
396                             .width(size)
397                             // This could be any (width in height out child) e.g. text
398                             .aspectRatio(2f)
399                             .onGloballyPositioned { coordinates ->
400                                 aspectRatioBoxSize.value = coordinates.size
401                             }
402                     )
403                     Box(
404                         Modifier.constrainAs(divider) {
405                                 centerTo(parent)
406                                 width = Dimension.value(1.dp)
407                                 height = Dimension.fillToConstraints
408                             }
409                             .onGloballyPositioned { coordinates ->
410                                 dividerSize.value = coordinates.size
411                             }
412                     )
413                 }
414             }
415 
416             rule.runOnIdle {
417                 // The width of the ConstraintLayout should be twice the width of the aspect ratio
418                 // box.
419                 assertEquals(size.roundToPx() * 2, constraintLayoutSize.value!!.width)
420                 // The height of the ConstraintLayout should be the height of the aspect ratio box.
421                 assertEquals(size.roundToPx() / 2, constraintLayoutSize.value!!.height)
422                 // The aspect ratio gets the requested size.
423                 assertEquals(size.roundToPx(), aspectRatioBoxSize.value!!.width)
424                 // Aspect ratio is preserved.
425                 assertEquals(size.roundToPx() / 2, aspectRatioBoxSize.value!!.height)
426                 // Divider has fixed width 1.dp in constraint set.
427                 assertEquals(1.dp.roundToPx(), dividerSize.value!!.width)
428                 // Divider should have the height of the aspect ratio box.
429                 assertEquals(aspectRatioBoxSize.value!!.height, dividerSize.value!!.height)
430             }
431         }
432 
433     // endregion
434 
435     // region positioning tests
436 
437     @Test
438     fun testConstraintLayout_withInlineDSL() =
439         with(rule.density) {
440             var rootSize: IntSize = IntSize.Zero
441             val boxSize = 100
442             val offset = 150
443 
444             val position: Array<IntOffset> = Array(3) { IntOffset.Zero }
445 
446             rule.setContent {
447                 ConstraintLayout(
448                     Modifier.fillMaxSize().onGloballyPositioned { rootSize = it.size }
449                 ) {
450                     val (box0, box1, box2) = createRefs()
451                     Box(
452                         Modifier.constrainAs(box0) { centerTo(parent) }
453                             .size(boxSize.toDp(), boxSize.toDp())
454                             .onGloballyPositioned { position[0] = it.positionInRoot().round() }
455                     )
456                     val half = createGuidelineFromAbsoluteLeft(fraction = 0.5f)
457                     Box(
458                         Modifier.constrainAs(box1) {
459                                 start.linkTo(half, margin = offset.toDp())
460                                 bottom.linkTo(box0.top)
461                             }
462                             .size(boxSize.toDp(), boxSize.toDp())
463                             .onGloballyPositioned { position[1] = it.positionInRoot().round() }
464                     )
465                     Box(
466                         Modifier.constrainAs(box2) {
467                                 start.linkTo(parent.start, margin = offset.toDp())
468                                 bottom.linkTo(parent.bottom, margin = offset.toDp())
469                             }
470                             .size(boxSize.toDp(), boxSize.toDp())
471                             .onGloballyPositioned { position[2] = it.positionInRoot().round() }
472                     )
473                 }
474             }
475 
476             val displayWidth = rootSize.width
477             val displayHeight = rootSize.height
478 
479             rule.runOnIdle {
480                 assertEquals(
481                     Offset(((displayWidth - boxSize) / 2f), ((displayHeight - boxSize) / 2f))
482                         .round(),
483                     position[0]
484                 )
485                 assertEquals(
486                     Offset((displayWidth / 2f + offset), ((displayHeight - boxSize) / 2f - boxSize))
487                         .round(),
488                     position[1]
489                 )
490                 assertEquals(IntOffset(offset, (displayHeight - boxSize - offset)), position[2])
491             }
492         }
493 
494     @Test
495     fun testConstraintLayout_withConstraintSet() =
496         with(rule.density) {
497             var rootSize: IntSize = IntSize.Zero
498             val boxSize = 100
499             val offset = 150
500 
501             val position: Array<IntOffset> = Array(3) { IntOffset.Zero }
502 
503             rule.setContent {
504                 ConstraintLayout(
505                     constraintSet =
506                         ConstraintSet {
507                             val box0 = createRefFor("box0")
508                             val box1 = createRefFor("box1")
509                             val box2 = createRefFor("box2")
510 
511                             constrain(box0) { centerTo(parent) }
512 
513                             val half = createGuidelineFromAbsoluteLeft(fraction = 0.5f)
514                             constrain(box1) {
515                                 start.linkTo(half, margin = offset.toDp())
516                                 bottom.linkTo(box0.top)
517                             }
518 
519                             constrain(box2) {
520                                 start.linkTo(parent.start, margin = offset.toDp())
521                                 bottom.linkTo(parent.bottom, margin = offset.toDp())
522                             }
523                         },
524                     modifier = Modifier.fillMaxSize().onGloballyPositioned { rootSize = it.size }
525                 ) {
526                     for (i in 0..2) {
527                         Box(
528                             Modifier.layoutId("box$i")
529                                 .size(boxSize.toDp(), boxSize.toDp())
530                                 .onGloballyPositioned { position[i] = it.positionInRoot().round() }
531                         )
532                     }
533                 }
534             }
535 
536             val displayWidth = rootSize.width
537             val displayHeight = rootSize.height
538 
539             rule.runOnIdle {
540                 assertEquals(
541                     Offset((displayWidth - boxSize) / 2f, (displayHeight - boxSize) / 2f).round(),
542                     position[0]
543                 )
544                 assertEquals(
545                     Offset((displayWidth / 2f + offset), ((displayHeight - boxSize) / 2f - boxSize))
546                         .round(),
547                     position[1]
548                 )
549                 assertEquals(IntOffset(offset, (displayHeight - boxSize - offset)), position[2])
550             }
551         }
552 
553     @Test
554     @Ignore
555     fun testConstraintLayout_rtl() =
556         with(rule.density) {
557             val boxSize = 100
558             val offset = 150
559 
560             val position = Array(3) { Ref<Offset>() }
561 
562             rule.setContent {
563                 CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
564                     ConstraintLayout(Modifier.fillMaxSize()) {
565                         val (box0, box1, box2) = createRefs()
566                         Box(
567                             Modifier.constrainAs(box0) { centerTo(parent) }
568                                 .size(boxSize.toDp(), boxSize.toDp())
569                                 .onGloballyPositioned { position[0].value = it.positionInRoot() }
570                         )
571                         val half = createGuidelineFromAbsoluteLeft(fraction = 0.5f)
572                         Box(
573                             Modifier.constrainAs(box1) {
574                                     start.linkTo(half, margin = offset.toDp())
575                                     bottom.linkTo(box0.top)
576                                 }
577                                 .size(boxSize.toDp(), boxSize.toDp())
578                                 .onGloballyPositioned { position[1].value = it.positionInRoot() }
579                         )
580                         Box(
581                             Modifier.constrainAs(box2) {
582                                     start.linkTo(parent.start, margin = offset.toDp())
583                                     bottom.linkTo(parent.bottom, margin = offset.toDp())
584                                 }
585                                 .size(boxSize.toDp(), boxSize.toDp())
586                                 .onGloballyPositioned { position[2].value = it.positionInRoot() }
587                         )
588                     }
589                 }
590             }
591 
592             val displayWidth = displaySize.width
593             val displayHeight = displaySize.height
594 
595             rule.runOnIdle {
596                 assertEquals(
597                     Offset((displayWidth - boxSize) / 2f, (displayHeight - boxSize) / 2f),
598                     position[0].value
599                 )
600                 assertEquals(
601                     Offset(
602                         (displayWidth / 2 - offset - boxSize).toFloat(),
603                         ((displayHeight - boxSize) / 2 - boxSize).toFloat()
604                     ),
605                     position[1].value
606                 )
607                 assertEquals(
608                     Offset(
609                         (displayWidth - offset - boxSize).toFloat(),
610                         (displayHeight - boxSize - offset).toFloat()
611                     ),
612                     position[2].value
613                 )
614             }
615         }
616 
617     @Test
618     fun testConstraintLayout_guidelines_ltr() =
619         with(rule.density) {
620             val size = 200.toDp()
621             val offset = 50.toDp()
622 
623             val position = Array(8) { 0f }
624             rule.setContent {
625                 ConstraintLayout(Modifier.size(size)) {
626                     val guidelines =
627                         arrayOf(
628                             createGuidelineFromStart(offset),
629                             createGuidelineFromAbsoluteLeft(offset),
630                             createGuidelineFromEnd(offset),
631                             createGuidelineFromAbsoluteRight(offset),
632                             createGuidelineFromStart(0.25f),
633                             createGuidelineFromAbsoluteLeft(0.25f),
634                             createGuidelineFromEnd(0.25f),
635                             createGuidelineFromAbsoluteRight(0.25f)
636                         )
637 
638                     guidelines.forEachIndexed { index, guideline ->
639                         val ref = createRef()
640                         Box(
641                             Modifier.size(1.dp)
642                                 .constrainAs(ref) { absoluteLeft.linkTo(guideline) }
643                                 .onGloballyPositioned { position[index] = it.positionInParent().x }
644                         )
645                     }
646                 }
647             }
648 
649             assertGuidelinesLtrPositions(position)
650         }
651 
652     @Test
653     fun testConstraintLayout_json_guidelines_ltr() =
654         with(rule.density) {
655             val size = 200.toDp()
656             val offset = 50.toDp()
657 
658             val position = Array(8) { 0f }
659             rule.setContent {
660                 ConstraintLayout(
661                     constraintSet = ConstraintSet(getJsonGuidelinesContent(offset.value)),
662                     Modifier.size(size)
663                 ) {
664                     position.forEachIndexed { index, _ ->
665                         Box(
666                             Modifier.size(1.dp).layoutId("box$index").onGloballyPositioned {
667                                 position[index] = it.positionInParent().x
668                             }
669                         )
670                     }
671                 }
672             }
673 
674             assertGuidelinesLtrPositions(position)
675         }
676 
677     @Test
678     fun testConstraintLayout_guidelines_rtl() =
679         with(rule.density) {
680             val size = 200.toDp()
681             val offset = 50.toDp()
682 
683             val position = Array(8) { 0f }
684             rule.setContent {
685                 CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
686                     ConstraintLayout(Modifier.size(size)) {
687                         val guidelines =
688                             arrayOf(
689                                 createGuidelineFromStart(offset),
690                                 createGuidelineFromAbsoluteLeft(offset),
691                                 createGuidelineFromEnd(offset),
692                                 createGuidelineFromAbsoluteRight(offset),
693                                 createGuidelineFromStart(0.25f),
694                                 createGuidelineFromAbsoluteLeft(0.25f),
695                                 createGuidelineFromEnd(0.25f),
696                                 createGuidelineFromAbsoluteRight(0.25f)
697                             )
698 
699                         guidelines.forEachIndexed { index, guideline ->
700                             val ref = createRef()
701                             Box(
702                                 Modifier.size(1.dp)
703                                     .constrainAs(ref) { absoluteLeft.linkTo(guideline) }
704                                     .onGloballyPositioned {
705                                         position[index] = it.positionInParent().x
706                                     }
707                             )
708                         }
709                     }
710                 }
711             }
712 
713             assertGuidelinesRtlPositions(position)
714         }
715 
716     @Test
717     fun testConstraintLayout_json_guidelines_rtl() =
718         with(rule.density) {
719             val size = 200.toDp()
720             val offset = 50.toDp()
721 
722             val position = Array(8) { 0f }
723             rule.setContent {
724                 CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
725                     ConstraintLayout(
726                         constraintSet = ConstraintSet(getJsonGuidelinesContent(offset.value)),
727                         Modifier.size(size)
728                     ) {
729                         position.forEachIndexed { index, _ ->
730                             Box(
731                                 Modifier.size(1.dp).layoutId("box$index").onGloballyPositioned {
732                                     position[index] = it.positionInParent().x
733                                 }
734                             )
735                         }
736                     }
737                 }
738             }
739 
740             assertGuidelinesRtlPositions(position)
741         }
742 
743     @Test
744     fun testConstraintLayout_barriers_ltr() =
745         with(rule.density) {
746             val size = 200.toDp()
747             val offset = 50.toDp()
748 
749             val position = Array(4) { 0f }
750             rule.setContent {
751                 ConstraintLayout(Modifier.size(size)) {
752                     val (box1, box2) = createRefs()
753                     val guideline1 = createGuidelineFromAbsoluteLeft(offset)
754                     val guideline2 = createGuidelineFromAbsoluteRight(offset)
755                     Box(
756                         Modifier.size(1.toDp()).constrainAs(box1) {
757                             absoluteLeft.linkTo(guideline1)
758                         }
759                     )
760                     Box(
761                         Modifier.size(1.toDp()).constrainAs(box2) {
762                             absoluteLeft.linkTo(guideline2)
763                         }
764                     )
765 
766                     val barriers =
767                         arrayOf(
768                             createStartBarrier(box1, box2),
769                             createAbsoluteLeftBarrier(box1, box2),
770                             createEndBarrier(box1, box2),
771                             createAbsoluteRightBarrier(box1, box2)
772                         )
773 
774                     barriers.forEachIndexed { index, barrier ->
775                         val ref = createRef()
776                         Box(
777                             Modifier.size(1.dp)
778                                 .constrainAs(ref) { absoluteLeft.linkTo(barrier) }
779                                 .onGloballyPositioned { position[index] = it.positionInParent().x }
780                         )
781                     }
782                 }
783             }
784 
785             assertBarriersLtrPositions(position)
786         }
787 
788     @Test
789     fun testConstraintLayout_json_barriers_ltr() =
790         with(rule.density) {
791             val size = 200.toDp()
792             val offset = 50.toDp()
793 
794             val position = Array(4) { 0f }
795             rule.setContent {
796                 ConstraintLayout(
797                     constraintSet = ConstraintSet(getJsonBarriersContent(offset.value)),
798                     Modifier.size(size)
799                 ) {
800                     Box(Modifier.size(1.toDp()).layoutId("boxA"))
801                     Box(Modifier.size(1.toDp()).layoutId("boxB"))
802                     position.forEachIndexed { index, _ ->
803                         Box(
804                             Modifier.size(1.dp).layoutId("box$index").onGloballyPositioned {
805                                 position[index] = it.positionInParent().x
806                             }
807                         )
808                     }
809                 }
810             }
811 
812             assertBarriersLtrPositions(position)
813         }
814 
815     @Test
816     fun testConstraintLayout_barriers_rtl() =
817         with(rule.density) {
818             val size = 200.toDp()
819             val offset = 50.toDp()
820 
821             val position = Array(4) { 0f }
822             rule.setContent {
823                 CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
824                     ConstraintLayout(Modifier.size(size)) {
825                         val (box1, box2) = createRefs()
826                         val guideline1 = createGuidelineFromAbsoluteLeft(offset)
827                         val guideline2 = createGuidelineFromAbsoluteRight(offset)
828                         Box(
829                             Modifier.size(1.toDp()).constrainAs(box1) {
830                                 absoluteLeft.linkTo(guideline1)
831                             }
832                         )
833                         Box(
834                             Modifier.size(1.toDp()).constrainAs(box2) {
835                                 absoluteLeft.linkTo(guideline2)
836                             }
837                         )
838 
839                         val barriers =
840                             arrayOf(
841                                 createStartBarrier(box1, box2),
842                                 createAbsoluteLeftBarrier(box1, box2),
843                                 createEndBarrier(box1, box2),
844                                 createAbsoluteRightBarrier(box1, box2)
845                             )
846 
847                         barriers.forEachIndexed { index, barrier ->
848                             val ref = createRef()
849                             Box(
850                                 Modifier.size(1.dp)
851                                     .constrainAs(ref) { absoluteLeft.linkTo(barrier) }
852                                     .onGloballyPositioned {
853                                         position[index] = it.positionInParent().x
854                                     }
855                             )
856                         }
857                     }
858                 }
859             }
860 
861             assertBarriersRtlPositions(position)
862         }
863 
864     @Test
865     fun testConstraintLayout_json_barriers_rtl() =
866         with(rule.density) {
867             val size = 200.toDp()
868             val offset = 50.toDp()
869 
870             val position = Array(4) { 0f }
871             rule.setContent {
872                 CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
873                     ConstraintLayout(
874                         constraintSet = ConstraintSet(getJsonBarriersContent(offset.value)),
875                         Modifier.size(size)
876                     ) {
877                         Box(Modifier.size(1.toDp()).layoutId("boxA"))
878                         Box(Modifier.size(1.toDp()).layoutId("boxB"))
879                         position.forEachIndexed { index, _ ->
880                             Box(
881                                 Modifier.size(1.dp).layoutId("box$index").onGloballyPositioned {
882                                     position[index] = it.positionInParent().x
883                                 }
884                             )
885                         }
886                     }
887                 }
888             }
889 
890             assertBarriersRtlPositions(position)
891         }
892 
893     @Test
894     fun testConstraintLayout_anchors_ltr() =
895         with(rule.density) {
896             val size = 200.toDp()
897             val offset = 50.toDp()
898 
899             val position = Array(16) { 0f }
900             rule.setContent {
901                 ConstraintLayout(Modifier.size(size)) {
902                     val box = createRef()
903                     val guideline = createGuidelineFromAbsoluteLeft(offset)
904                     Box(Modifier.size(1.toDp()).constrainAs(box) { absoluteLeft.linkTo(guideline) })
905 
906                     val anchors = listAnchors(box)
907 
908                     anchors.forEachIndexed { index, anchor ->
909                         val ref = createRef()
910                         Box(
911                             Modifier.size(1.toDp())
912                                 .constrainAs(ref) { anchor() }
913                                 .onGloballyPositioned { position[index] = it.positionInParent().x }
914                         )
915                     }
916                 }
917             }
918 
919             assertAnchorsLtrPositions(position)
920         }
921 
922     @Test
923     fun testConstraintLayout_json_anchors_ltr() =
924         with(rule.density) {
925             val size = 200.toDp()
926             val offset = 50.toDp()
927 
928             val position = Array(16) { 0f }
929             rule.setContent {
930                 ConstraintLayout(
931                     constraintSet = ConstraintSet(getJsonAnchorsContent(offset.value)),
932                     Modifier.size(size)
933                 ) {
934                     Box(Modifier.size(1.toDp()).layoutId("box"))
935                     position.forEachIndexed { index, _ ->
936                         Box(
937                             Modifier.size(1.toDp()).layoutId("box$index").onGloballyPositioned {
938                                 position[index] = it.positionInParent().x
939                             }
940                         )
941                     }
942                 }
943             }
944 
945             assertAnchorsLtrPositions(position)
946         }
947 
948     @Test
949     fun testConstraintLayout_anchors_rtl() =
950         with(rule.density) {
951             val size = 200.toDp()
952             val offset = 50.toDp()
953 
954             val position = Array(16) { 0f }
955             rule.setContent {
956                 CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
957                     ConstraintLayout(Modifier.size(size)) {
958                         val box = createRef()
959                         val guideline = createGuidelineFromAbsoluteLeft(offset)
960                         Box(
961                             Modifier.size(1.toDp()).constrainAs(box) {
962                                 absoluteLeft.linkTo(guideline)
963                             }
964                         )
965 
966                         val anchors = listAnchors(box)
967 
968                         anchors.forEachIndexed { index, anchor ->
969                             val ref = createRef()
970                             Box(
971                                 Modifier.size(1.toDp())
972                                     .constrainAs(ref) { anchor() }
973                                     .onGloballyPositioned {
974                                         position[index] = it.positionInParent().x
975                                     }
976                             )
977                         }
978                     }
979                 }
980             }
981 
982             assertAnchorsRtlPositions(position)
983         }
984 
985     @Test
986     fun testConstraintLayout_json_anchors_rtl() =
987         with(rule.density) {
988             val size = 200.toDp()
989             val offset = 50.toDp()
990 
991             val position = Array(16) { 0f }
992             rule.setContent {
993                 CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
994                     ConstraintLayout(
995                         constraintSet = ConstraintSet(getJsonAnchorsContent(offset.value)),
996                         Modifier.size(size)
997                     ) {
998                         Box(Modifier.size(1.toDp()).layoutId("box"))
999                         position.forEachIndexed { index, _ ->
1000                             Box(
1001                                 Modifier.size(1.toDp()).layoutId("box$index").onGloballyPositioned {
1002                                     position[index] = it.positionInParent().x
1003                                 }
1004                             )
1005                         }
1006                     }
1007                 }
1008             }
1009 
1010             assertAnchorsRtlPositions(position)
1011         }
1012 
1013     @Test
1014     fun testConstraintLayout_barriers_margins() =
1015         with(rule.density) {
1016             val size = 200.toDp()
1017             val offset = 50.toDp()
1018 
1019             val position = Array(2) { Offset(0f, 0f) }
1020             rule.setContent {
1021                 ConstraintLayout(Modifier.size(size)) {
1022                     val box = createRef()
1023                     val guideline1 = createGuidelineFromAbsoluteLeft(offset)
1024                     val guideline2 = createGuidelineFromTop(offset)
1025                     Box(
1026                         Modifier.size(1.toDp()).constrainAs(box) {
1027                             absoluteLeft.linkTo(guideline1)
1028                             top.linkTo(guideline2)
1029                         }
1030                     )
1031 
1032                     val leftBarrier = createAbsoluteLeftBarrier(box, margin = 10.toDp())
1033                     val topBarrier = createTopBarrier(box, margin = 10.toDp())
1034                     val rightBarrier = createAbsoluteRightBarrier(box, margin = 10.toDp())
1035                     val bottomBarrier = createBottomBarrier(box, margin = 10.toDp())
1036 
1037                     Box(
1038                         Modifier.size(1.dp)
1039                             .constrainAs(createRef()) {
1040                                 absoluteLeft.linkTo(leftBarrier)
1041                                 top.linkTo(topBarrier)
1042                             }
1043                             .onGloballyPositioned { position[0] = it.positionInParent() }
1044                     )
1045 
1046                     Box(
1047                         Modifier.size(1.dp)
1048                             .constrainAs(createRef()) {
1049                                 absoluteLeft.linkTo(rightBarrier)
1050                                 top.linkTo(bottomBarrier)
1051                             }
1052                             .onGloballyPositioned { position[1] = it.positionInParent() }
1053                     )
1054                 }
1055             }
1056 
1057             rule.runOnIdle {
1058                 assertEquals(Offset(60f, 60f), position[0])
1059                 assertEquals(Offset(61f, 61f), position[1])
1060             }
1061         }
1062 
1063     @Test
1064     fun links_canBeOverridden() {
1065         rule.setContent {
1066             ConstraintLayout(Modifier.width(10.dp)) {
1067                 val box = createRef()
1068                 Box(
1069                     Modifier.constrainAs(box) {
1070                             start.linkTo(parent.end)
1071                             start.linkTo(parent.start)
1072                         }
1073                         .onGloballyPositioned { assertEquals(0f, it.positionInParent().x) }
1074                 )
1075             }
1076         }
1077         rule.waitForIdle()
1078     }
1079 
1080     @Test
1081     @Ignore
1082     fun chains_defaultOutsideConstraintsCanBeOverridden() =
1083         with(rule.density) {
1084             val size = 100.toDp()
1085             val boxSize = 10.toDp()
1086             val guidelinesOffset = 20.toDp()
1087             rule.setContent {
1088                 ConstraintLayout(Modifier.size(size)) {
1089                     val (box1, box2) = createRefs()
1090                     val startGuideline = createGuidelineFromStart(guidelinesOffset)
1091                     val topGuideline = createGuidelineFromTop(guidelinesOffset)
1092                     val endGuideline = createGuidelineFromEnd(guidelinesOffset)
1093                     val bottomGuideline = createGuidelineFromBottom(guidelinesOffset)
1094                     createHorizontalChain(box1, box2, chainStyle = ChainStyle.SpreadInside)
1095                     createVerticalChain(box1, box2, chainStyle = ChainStyle.SpreadInside)
1096                     Box(
1097                         Modifier.size(boxSize)
1098                             .constrainAs(box1) {
1099                                 start.linkTo(startGuideline)
1100                                 top.linkTo(topGuideline)
1101                             }
1102                             .onGloballyPositioned {
1103                                 assertEquals(20f, it.boundsInParent().left)
1104                                 assertEquals(20f, it.boundsInParent().top)
1105                             }
1106                     )
1107                     Box(
1108                         Modifier.size(boxSize)
1109                             .constrainAs(box2) {
1110                                 end.linkTo(endGuideline)
1111                                 bottom.linkTo(bottomGuideline)
1112                             }
1113                             .onGloballyPositioned {
1114                                 assertEquals(80f, it.boundsInParent().right)
1115                                 assertEquals(80f, it.boundsInParent().bottom)
1116                             }
1117                     )
1118                 }
1119             }
1120             rule.waitForIdle()
1121         }
1122 
1123     @Test(expected = Test.None::class)
1124     fun testConstraintLayout_inlineDSL_recompositionDoesNotCrash() {
1125         val first = mutableStateOf(true)
1126         rule.setContent {
1127             ConstraintLayout {
1128                 val box = createRef()
1129                 if (first.value) {
1130                     Box(Modifier.constrainAs(box) {})
1131                 } else {
1132                     Box(Modifier.constrainAs(box) {})
1133                 }
1134             }
1135         }
1136         rule.runOnIdle { first.value = false }
1137         rule.waitForIdle()
1138     }
1139 
1140     @Test(expected = Test.None::class)
1141     fun testConstraintLayout_ConstraintSetDSL_recompositionDoesNotCrash() {
1142         val first = mutableStateOf(true)
1143         rule.setContent {
1144             ConstraintLayout(
1145                 ConstraintSet {
1146                     val box = createRefFor("box")
1147                     constrain(box) {}
1148                 }
1149             ) {
1150                 if (first.value) {
1151                     Box(Modifier.layoutId("box"))
1152                 } else {
1153                     Box(Modifier.layoutId("box"))
1154                 }
1155             }
1156         }
1157         rule.runOnIdle { first.value = false }
1158         rule.waitForIdle()
1159     }
1160 
1161     @Test(expected = Test.None::class)
1162     fun testConstraintLayout_inlineDSL_remeasureDoesNotCrash() {
1163         val first = mutableStateOf(true)
1164         rule.setContent {
1165             ConstraintLayout(if (first.value) Modifier else Modifier.padding(10.dp)) {
1166                 Box(if (first.value) Modifier else Modifier.size(20.dp))
1167             }
1168         }
1169         rule.runOnIdle { first.value = false }
1170         rule.waitForIdle()
1171     }
1172 
1173     @Test(expected = Test.None::class)
1174     fun testConstraintLayout_ConstraintSetDSL_remeasureDoesNotCrash() {
1175         val first = mutableStateOf(true)
1176         rule.setContent {
1177             ConstraintLayout(
1178                 modifier = if (first.value) Modifier else Modifier.padding(10.dp),
1179                 constraintSet = ConstraintSet {}
1180             ) {
1181                 Box(if (first.value) Modifier else Modifier.size(20.dp))
1182             }
1183         }
1184         rule.runOnIdle { first.value = false }
1185         rule.waitForIdle()
1186     }
1187 
1188     @Test
1189     fun testConstraintLayout_doesNotCrashWhenOnlyContentIsRecomposed() {
1190         var smallSize by mutableStateOf(true)
1191         rule.setContent {
1192             Box {
1193                 ConstraintLayout {
1194                     val (box1, _) = createRefs()
1195                     createBottomBarrier(box1)
1196                     Box(Modifier.height(if (smallSize) 30.dp else 40.dp).constrainAs(box1) {})
1197                     Box(Modifier)
1198                 }
1199             }
1200         }
1201         rule.runOnIdle { smallSize = false }
1202         rule.waitForIdle()
1203     }
1204 
1205     @Test
1206     fun testInspectorValue() {
1207         rule.setContent {
1208             ConstraintLayout(Modifier.width(10.dp)) {
1209                 val ref = createRef()
1210                 val block: ConstrainScope.() -> Unit = {}
1211                 val modifier = Modifier.constrainAs(ref, block) as InspectableValue
1212 
1213                 assertEquals("constrainAs", modifier.nameFallback)
1214                 assertNull(modifier.valueOverride)
1215                 val inspectableElements = modifier.inspectableElements.toList()
1216                 assertEquals(2, inspectableElements.size)
1217                 assertEquals(ValueElement("ref", ref), inspectableElements[0])
1218                 assertEquals(ValueElement("constrainBlock", block), inspectableElements[1])
1219             }
1220         }
1221     }
1222 
1223     @Test
1224     fun testConstraintLayout_doesNotRemeasureUnnecessarily() {
1225         var first by mutableStateOf(true)
1226         var dslExecutions = 0
1227         rule.setContent {
1228             val dslExecuted = remember { { ++dslExecutions } }
1229             ConstraintLayout {
1230                 val (box1) = createRefs()
1231                 val box2 = createRef()
1232                 val guideline = createGuidelineFromStart(0.5f)
1233                 val barrier = createAbsoluteLeftBarrier(box1)
1234 
1235                 // Make sure the content is reexecuted when first changes.
1236                 @Suppress("UNUSED_EXPRESSION") first
1237 
1238                 // If the reference changed, we would remeasure and reexecute the DSL.
1239                 Box(Modifier.constrainAs(box1) {})
1240                 // If the guideline, barrier or anchor changed or were inferred as un@Stable, we
1241                 // would remeasure and reexecute the DSL.
1242                 Box(
1243                     Modifier.constrainAs(box2) {
1244                         start.linkTo(box1.end)
1245                         end.linkTo(guideline)
1246                         start.linkTo(barrier)
1247                         dslExecuted()
1248                     }
1249                 )
1250             }
1251         }
1252         rule.runOnIdle {
1253             assertEquals(1, dslExecutions)
1254             first = false
1255         }
1256         rule.runOnIdle { assertEquals(1, dslExecutions) }
1257     }
1258 
1259     @Test
1260     fun testConstraintLayout_doesRemeasure_whenHelpersChange_butConstraintsDont() {
1261         val size = 100
1262         val sizeDp = with(rule.density) { size.toDp() }
1263         var first by mutableStateOf(true)
1264         var box1Position = Offset(-1f, -1f)
1265         var box2Position = Offset(-1f, -1f)
1266         val box1PositionUpdater =
1267             Modifier.onGloballyPositioned { box1Position = it.positionInRoot() }
1268         val box2PositionUpdater =
1269             Modifier.onGloballyPositioned { box2Position = it.positionInRoot() }
1270         rule.setContent {
1271             ConstraintLayout {
1272                 val (box1, box2) = createRefs()
1273 
1274                 if (!first) {
1275                     createVerticalChain(box1, box2)
1276                 }
1277 
1278                 Box(Modifier.size(sizeDp).then(box1PositionUpdater).constrainAs(box1) {})
1279                 Box(Modifier.size(sizeDp).then(box2PositionUpdater).constrainAs(box2) {})
1280             }
1281         }
1282         rule.runOnIdle {
1283             assertEquals(Offset.Zero, box1Position)
1284             assertEquals(Offset.Zero, box2Position)
1285             first = false
1286         }
1287         rule.runOnIdle {
1288             assertEquals(Offset.Zero, box1Position)
1289             assertEquals(Offset(0f, size.toFloat()), box2Position)
1290         }
1291     }
1292 
1293     @Test
1294     fun testConstraintLayout_doesRemeasure_whenHelpersDontChange_butConstraintsDo() {
1295         val size = 100
1296         val sizeDp = with(rule.density) { size.toDp() }
1297         var first by mutableStateOf(true)
1298         var box1Position = Offset(-1f, -1f)
1299         var box2Position = Offset(-1f, -1f)
1300         val box1PositionUpdater =
1301             Modifier.onGloballyPositioned { box1Position = it.positionInRoot() }
1302         val box2PositionUpdater =
1303             Modifier.onGloballyPositioned { box2Position = it.positionInRoot() }
1304         rule.setContent {
1305             ConstraintLayout {
1306                 val (box1, box2) = createRefs()
1307 
1308                 val topBarrier = createTopBarrier(box1)
1309                 val bottomBarrier = createBottomBarrier(box1)
1310 
1311                 Box(Modifier.size(sizeDp).then(box1PositionUpdater).constrainAs(box1) {})
1312                 Box(
1313                     Modifier.size(sizeDp).then(box2PositionUpdater).constrainAs(box2) {
1314                         if (first) {
1315                             top.linkTo(topBarrier)
1316                         } else {
1317                             top.linkTo(bottomBarrier)
1318                         }
1319                     }
1320                 )
1321             }
1322         }
1323         rule.runOnIdle {
1324             assertEquals(Offset.Zero, box1Position)
1325             assertEquals(Offset.Zero, box2Position)
1326             first = false
1327         }
1328         rule.runOnIdle {
1329             assertEquals(Offset.Zero, box1Position)
1330             assertEquals(Offset(0f, size.toFloat()), box2Position)
1331         }
1332     }
1333 
1334     @Test
1335     fun testConstraintLayout_updates_whenConstraintSetChanges() =
1336         with(rule.density) {
1337             val box1Size = 20
1338             var first by mutableStateOf(true)
1339             val constraintSet1 = ConstraintSet {
1340                 val box1 = createRefFor("box1")
1341                 val box2 = createRefFor("box2")
1342                 constrain(box2) { start.linkTo(box1.end) }
1343             }
1344             val constraintSet2 = ConstraintSet {
1345                 val box1 = createRefFor("box1")
1346                 val box2 = createRefFor("box2")
1347                 constrain(box2) { top.linkTo(box1.bottom) }
1348             }
1349 
1350             var box2Position = IntOffset.Zero
1351             rule.setContent {
1352                 ConstraintLayout(if (first) constraintSet1 else constraintSet2) {
1353                     Box(Modifier.size(box1Size.toDp()).layoutId("box1"))
1354                     Box(
1355                         Modifier.layoutId("box2").onGloballyPositioned {
1356                             box2Position = it.positionInRoot().round()
1357                         }
1358                     )
1359                 }
1360             }
1361 
1362             rule.runOnIdle {
1363                 assertEquals(IntOffset(box1Size, 0), box2Position)
1364                 first = false
1365             }
1366 
1367             rule.runOnIdle { assertEquals(IntOffset(0, box1Size), box2Position) }
1368         }
1369 
1370     @Test
1371     fun testConstraintLayout_doesNotRebuildFromDsl_whenResizedOnly() {
1372         var size by mutableStateOf(100.dp)
1373         var builds = 0
1374         rule.setContent {
1375             val onBuild = remember { { ++builds } }
1376             ConstraintLayout(Modifier.size(size)) {
1377                 val box = createRef()
1378                 Box(Modifier.constrainAs(box) { onBuild() })
1379             }
1380         }
1381 
1382         rule.runOnIdle {
1383             assertEquals(1, builds)
1384             size = 200.dp
1385         }
1386 
1387         rule.runOnIdle { assertEquals(1, builds) }
1388     }
1389 
1390     @Test
1391     fun testConstraintLayout_rebuildsConstraintSet_whenHelpersChange() =
1392         with(rule.density) {
1393             var offset by mutableStateOf(10.dp)
1394             var builds = 0
1395             var obtainedX = 0f
1396             rule.setContent {
1397                 ConstraintLayout {
1398                     val box = createRef()
1399                     val g = createGuidelineFromStart(offset)
1400                     Box(
1401                         Modifier.constrainAs(box) {
1402                                 start.linkTo(g)
1403                                 ++builds
1404                             }
1405                             .onGloballyPositioned { obtainedX = it.positionInRoot().x }
1406                     )
1407                 }
1408             }
1409 
1410             rule.runOnIdle {
1411                 assertEquals(offset.roundToPx().toFloat(), obtainedX)
1412                 offset = 20.dp
1413                 assertEquals(1, builds)
1414             }
1415 
1416             rule.runOnIdle {
1417                 assertEquals(offset.roundToPx().toFloat(), obtainedX)
1418                 assertEquals(2, builds)
1419             }
1420         }
1421 
1422     @Test
1423     fun testConstraintLayout_doesNotRecomposeAgain_whenHelpersChange() {
1424         var offset by mutableStateOf(10.dp)
1425         var compositions = 0
1426         rule.setContent {
1427             ConstraintLayout {
1428                 ++compositions
1429                 val box = createRef()
1430                 val g = createGuidelineFromStart(offset)
1431                 Box(Modifier.constrainAs(box) { start.linkTo(g) })
1432             }
1433         }
1434 
1435         rule.runOnIdle {
1436             offset = 20.dp
1437             assertEquals(1, compositions)
1438         }
1439 
1440         rule.runOnIdle { assertEquals(2, compositions) }
1441     }
1442 
1443     @Test
1444     fun testConstraintLayout_rebuilds_whenLambdaChanges() =
1445         with(rule.density) {
1446             var first by mutableStateOf(true)
1447             var obtainedX = 0f
1448             rule.setContent {
1449                 ConstraintLayout {
1450                     val l1 =
1451                         remember<ConstrainScope.() -> Unit> {
1452                             { start.linkTo(parent.start, 10.dp) }
1453                         }
1454                     val l2 =
1455                         remember<ConstrainScope.() -> Unit> {
1456                             { start.linkTo(parent.start, 20.dp) }
1457                         }
1458                     val box = createRef()
1459                     Box(
1460                         Modifier.constrainAs(box, if (first) l1 else l2).onGloballyPositioned {
1461                             obtainedX = it.positionInRoot().x
1462                         }
1463                     )
1464                 }
1465             }
1466 
1467             rule.runOnIdle {
1468                 assertEquals(10.dp.roundToPx().toFloat(), obtainedX)
1469                 first = false
1470             }
1471 
1472             rule.runOnIdle { assertEquals(20.dp.roundToPx().toFloat(), obtainedX) }
1473         }
1474 
1475     @Test
1476     fun testConstraintLayout_updates_whenConstraintSetChangesConstraints() =
1477         with(rule.density) {
1478             val box1Size = 20
1479             var first by mutableStateOf(true)
1480 
1481             var box2Position = IntOffset.Zero
1482             rule.setContent {
1483                 val constraintSet = ConstraintSet {
1484                     val box1 = createRefFor("box1")
1485                     val box2 = createRefFor("box2")
1486                     constrain(box2) {
1487                         if (first) start.linkTo(box1.end) else top.linkTo(box1.bottom)
1488                     }
1489                 }
1490                 ConstraintLayout(constraintSet) {
1491                     Box(Modifier.size(box1Size.toDp()).layoutId("box1"))
1492                     Box(
1493                         Modifier.layoutId("box2").onGloballyPositioned {
1494                             box2Position = it.positionInRoot().round()
1495                         }
1496                     )
1497                 }
1498             }
1499 
1500             rule.runOnIdle {
1501                 assertEquals(IntOffset(box1Size, 0), box2Position)
1502                 first = false
1503             }
1504 
1505             rule.runOnIdle { assertEquals(IntOffset(0, box1Size), box2Position) }
1506         }
1507 
1508     @Test
1509     fun testConstraintLayout_doesNotUpdate_withRememberConstraintSet() =
1510         with(rule.density) {
1511             val box1Size = 20
1512             var first by mutableStateOf(true)
1513             var compCount = 0
1514 
1515             var box2Position = IntOffset.Zero
1516             rule.setContent {
1517                 // ConstraintSet should be immutable and shouldn't recompose if "remembered"
1518                 val constraintSet = remember {
1519                     ConstraintSet {
1520                         val box1 = createRefFor("box1")
1521                         val box2 = createRefFor("box2")
1522                         constrain(box2) {
1523                             if (first) start.linkTo(box1.end) else top.linkTo(box1.bottom)
1524                         }
1525                     }
1526                 }
1527                 compCount++
1528                 ConstraintLayout(constraintSet) {
1529                     Box(Modifier.size(box1Size.toDp()).layoutId("box1"))
1530                     Box(
1531                         Modifier.layoutId("box2").onGloballyPositioned {
1532                             box2Position = it.positionInRoot().round()
1533                         }
1534                     )
1535                 }
1536             }
1537 
1538             rule.runOnIdle {
1539                 assertEquals(IntOffset(box1Size, 0), box2Position)
1540                 assertEquals(1, compCount)
1541                 first = false
1542             }
1543 
1544             rule.runOnIdle {
1545                 assertEquals(IntOffset(box1Size, 0), box2Position)
1546                 assertEquals(2, compCount)
1547             }
1548         }
1549 
1550     @Test
1551     fun testClearDerivedConstraints_withConstraintSet() {
1552         var startOrEnd by mutableStateOf(true)
1553         val boxTag = "box1"
1554         rule.setContent {
1555             val start = remember {
1556                 ConstraintSet {
1557                     constrain(createRefFor(boxTag)) {
1558                         width = Dimension.value(20.dp)
1559                         height = Dimension.value(20.dp)
1560                         start.linkTo(parent.start, 10.dp)
1561                         bottom.linkTo(parent.bottom, 10.dp)
1562                     }
1563                 }
1564             }
1565             val end = remember {
1566                 ConstraintSet(start) {
1567                     constrain(createRefFor(boxTag)) {
1568                         clearConstraints()
1569                         top.linkTo(parent.top, 5.dp)
1570                         end.linkTo(parent.end, 5.dp)
1571                     }
1572                 }
1573             }
1574             ConstraintLayout(
1575                 modifier = Modifier.size(200.dp),
1576                 constraintSet = if (startOrEnd) start else end
1577             ) {
1578                 Box(modifier = Modifier.background(Color.Red).testTag(boxTag).layoutId(boxTag))
1579             }
1580         }
1581         rule.waitForIdle()
1582         rule.onNodeWithTag(boxTag).assertPositionInRootIsEqualTo(10.dp, 170.dp)
1583         rule.runOnIdle { startOrEnd = !startOrEnd }
1584         rule.waitForIdle()
1585         rule.onNodeWithTag(boxTag).assertPositionInRootIsEqualTo(175.dp, 5.dp)
1586     }
1587 
1588     @Test
1589     fun testLayoutReference_withConstraintSet() =
1590         with(rule.density) {
1591             val rootSizePx = 500
1592             val boxSizePx = 100
1593             val g1DistancePx = 50
1594             val marginToHelper = 10
1595 
1596             val boxTag1 = "box1"
1597             val boxTag2 = "box2"
1598 
1599             var boxPosition1 = IntOffset.Zero
1600             var boxPosition2 = IntOffset.Zero
1601 
1602             val constraintSet = ConstraintSet {
1603                 val box1 = createRefFor(boxTag1)
1604                 val g1 = createGuidelineFromEnd(g1DistancePx.toDp())
1605                 val b1 = createEndBarrier(g1.reference, box1)
1606 
1607                 constrain(box1) {
1608                     width = Dimension.value(boxSizePx.toDp())
1609                     height = Dimension.value(boxSizePx.toDp())
1610                     top.linkTo(parent.top)
1611                     end.linkTo(g1, marginToHelper.toDp())
1612                 }
1613                 constrain(createRefFor(boxTag2)) {
1614                     width = Dimension.value(boxSizePx.toDp())
1615                     height = Dimension.value(boxSizePx.toDp())
1616                     top.linkTo(parent.top)
1617                     start.linkTo(b1, marginToHelper.toDp())
1618                 }
1619             }
1620             rule.setContent {
1621                 ConstraintLayout(
1622                     modifier = Modifier.size(rootSizePx.toDp()),
1623                     constraintSet = constraintSet
1624                 ) {
1625                     Box(
1626                         modifier =
1627                             Modifier.layoutTestId(boxTag1)
1628                                 .background(Color.Red)
1629                                 .onGloballyPositioned { boxPosition1 = it.positionInRoot().round() }
1630                     )
1631                     Box(
1632                         modifier =
1633                             Modifier.layoutTestId(boxTag2)
1634                                 .background(Color.Blue)
1635                                 .onGloballyPositioned { boxPosition2 = it.positionInRoot().round() }
1636                     )
1637                 }
1638             }
1639             rule.waitForIdle()
1640 
1641             assertEquals(rootSizePx - g1DistancePx - marginToHelper - boxSizePx, boxPosition1.x)
1642             assertEquals(0, boxPosition1.y)
1643 
1644             assertEquals(rootSizePx - g1DistancePx + marginToHelper, boxPosition2.x)
1645             assertEquals(0, boxPosition2.y)
1646         }
1647 
1648     @Test
1649     fun testLayoutReference_withInlineDsl() =
1650         with(rule.density) {
1651             val rootSizePx = 500
1652             val boxSizePx = 100
1653             val g1DistancePx = 50
1654             val marginToHelper = 10
1655 
1656             val boxTag1 = "box1"
1657             val boxTag2 = "box2"
1658 
1659             var boxPosition1 = IntOffset.Zero
1660             var boxPosition2 = IntOffset.Zero
1661             rule.setContent {
1662                 ConstraintLayout(modifier = Modifier.size(rootSizePx.toDp())) {
1663                     val (box1, box2) = createRefs()
1664                     val g1 = createGuidelineFromEnd(g1DistancePx.toDp())
1665                     val b1 = createEndBarrier(g1.reference, box1)
1666                     Box(
1667                         modifier =
1668                             Modifier.constrainAs(box1) {
1669                                     width = Dimension.value(boxSizePx.toDp())
1670                                     height = Dimension.value(boxSizePx.toDp())
1671                                     top.linkTo(parent.top)
1672                                     end.linkTo(g1, marginToHelper.toDp())
1673                                 }
1674                                 .layoutTestId(boxTag1)
1675                                 .background(Color.Red)
1676                                 .onGloballyPositioned { boxPosition1 = it.positionInRoot().round() }
1677                     )
1678                     Box(
1679                         modifier =
1680                             Modifier.constrainAs(box2) {
1681                                     width = Dimension.value(boxSizePx.toDp())
1682                                     height = Dimension.value(boxSizePx.toDp())
1683                                     top.linkTo(parent.top)
1684                                     start.linkTo(b1, marginToHelper.toDp())
1685                                 }
1686                                 .layoutTestId(boxTag2)
1687                                 .background(Color.Blue)
1688                                 .onGloballyPositioned { boxPosition2 = it.positionInRoot().round() }
1689                     )
1690                 }
1691             }
1692             rule.waitForIdle()
1693 
1694             assertEquals(rootSizePx - g1DistancePx - marginToHelper - boxSizePx, boxPosition1.x)
1695             assertEquals(0, boxPosition1.y)
1696 
1697             assertEquals(rootSizePx - g1DistancePx + marginToHelper, boxPosition2.x)
1698             assertEquals(0, boxPosition2.y)
1699         }
1700 
1701     @Test
1702     fun testBias_withConstraintSet() {
1703         val rootSize = 100.dp
1704         val boxSize = 10.dp
1705         val horBias = 0.2f
1706         val verBias = 1f - horBias
1707         rule.setContent {
1708             ConstraintLayout(
1709                 modifier = Modifier.size(rootSize),
1710                 constraintSet =
1711                     ConstraintSet {
1712                         constrain(createRefFor("box")) {
1713                             width = Dimension.value(boxSize)
1714                             height = Dimension.value(boxSize)
1715 
1716                             centerTo(parent)
1717                             horizontalBias = horBias
1718                             verticalBias = verBias
1719                         }
1720                     }
1721             ) {
1722                 Box(modifier = Modifier.background(Color.Red).layoutTestId("box"))
1723             }
1724         }
1725         rule.waitForIdle()
1726         rule
1727             .onNodeWithTag("box")
1728             .assertPositionInRootIsEqualTo((rootSize - boxSize) * 0.2f, (rootSize - boxSize) * 0.8f)
1729     }
1730 
1731     @Test
1732     fun testBias_withInlineDsl() {
1733         val rootSize = 100.dp
1734         val boxSize = 10.dp
1735         val horBias = 0.2f
1736         val verBias = 1f - horBias
1737         rule.setContent {
1738             ConstraintLayout(Modifier.size(rootSize)) {
1739                 val box = createRef()
1740                 Box(
1741                     modifier =
1742                         Modifier.background(Color.Red)
1743                             .constrainAs(box) {
1744                                 width = Dimension.value(boxSize)
1745                                 height = Dimension.value(boxSize)
1746 
1747                                 centerTo(parent)
1748                                 horizontalBias = horBias
1749                                 verticalBias = verBias
1750                             }
1751                             .layoutTestId("box")
1752                 )
1753             }
1754         }
1755         rule.waitForIdle()
1756         rule
1757             .onNodeWithTag("box")
1758             .assertPositionInRootIsEqualTo((rootSize - boxSize) * 0.2f, (rootSize - boxSize) * 0.8f)
1759     }
1760 
1761     @Test
1762     fun testConstraintSet_multipleRefs() =
1763         with(rule.density) {
1764             val rootSize = 50
1765             val boxSize = 20
1766             val margin = 10
1767             val positions = Array(3) { IntOffset.Zero }
1768             rule.setContent {
1769                 ConstraintLayout(
1770                     constraintSet =
1771                         ConstraintSet {
1772                             // Note that not enough IDs were provided, box2 will have a generated ID
1773                             val (box0, box1, box2) = createRefsFor("box0", "box1")
1774 
1775                             constrain(box0, box1) {
1776                                 width = boxSize.toDp().asDimension()
1777                                 height = boxSize.toDp().asDimension()
1778                                 top.linkTo(parent.top, margin.toDp())
1779                                 start.linkTo(parent.start, margin.toDp())
1780                             }
1781                             constrain(box2) {
1782                                 width = boxSize.toDp().asDimension()
1783                                 height = boxSize.toDp().asDimension()
1784 
1785                                 top.linkTo(box0.bottom)
1786                                 start.linkTo(box0.end)
1787                             }
1788                         },
1789                     Modifier.size(rootSize.toDp())
1790                 ) {
1791                     Box(
1792                         Modifier.layoutId("box0").onGloballyPositioned {
1793                             positions[0] = it.positionInRoot().round()
1794                         }
1795                     )
1796                     Box(
1797                         Modifier.layoutId("box1").onGloballyPositioned {
1798                             positions[1] = it.positionInRoot().round()
1799                         }
1800                     )
1801                     Box(
1802                         Modifier
1803                             // Generated id, tho normally, the user wouldn't know what the ID is
1804                             .layoutId("androidx.constraintlayout.id0")
1805                             .onGloballyPositioned { positions[2] = it.positionInRoot().round() }
1806                     )
1807                 }
1808             }
1809             rule.waitForIdle()
1810             assertEquals(IntOffset(margin, margin), positions[0])
1811             assertEquals(IntOffset(margin, margin), positions[1])
1812             assertEquals(IntOffset(margin + boxSize, margin + boxSize), positions[2])
1813         }
1814 
1815     @Test
1816     fun testLinkToBias_withInlineDsl_rtl() =
1817         with(rule.density) {
1818             val rootSize = 200
1819             val boxSize = 20
1820             val box1Bias = 0.2f
1821             val box2Bias = 0.2f
1822 
1823             var box1Position = IntOffset.Zero
1824             var box2Position = IntOffset.Zero
1825             rule.setContent {
1826                 CompositionLocalProvider(LocalLayoutDirection.provides(LayoutDirection.Rtl)) {
1827                     ConstraintLayout(Modifier.size(rootSize.toDp())) {
1828                         val (box1Ref, box2Ref) = createRefs()
1829 
1830                         Box(
1831                             modifier =
1832                                 Modifier.background(Color.Red)
1833                                     .constrainAs(box1Ref) {
1834                                         width = Dimension.value(boxSize.toDp())
1835                                         height = Dimension.value(boxSize.toDp())
1836 
1837                                         centerTo(parent)
1838                                         horizontalBias = box1Bias // unaffected by Rtl
1839                                     }
1840                                     .onGloballyPositioned {
1841                                         box1Position = it.positionInRoot().round()
1842                                     }
1843                         )
1844                         Box(
1845                             modifier =
1846                                 Modifier.background(Color.Blue)
1847                                     .constrainAs(box2Ref) {
1848                                         width = Dimension.value(boxSize.toDp())
1849                                         height = Dimension.value(boxSize.toDp())
1850 
1851                                         top.linkTo(box1Ref.bottom)
1852                                         linkTo(
1853                                             start = parent.start,
1854                                             end = box1Ref.start,
1855                                             startMargin = 0.dp,
1856                                             endMargin = 0.dp,
1857                                             startGoneMargin = 0.dp,
1858                                             endGoneMargin = 0.dp,
1859                                             bias = box2Bias // affected by Rtl
1860                                         )
1861                                     }
1862                                     .onGloballyPositioned {
1863                                         box2Position = it.positionInRoot().round()
1864                                     }
1865                         )
1866                     }
1867                 }
1868             }
1869 
1870             rule.runOnIdle {
1871                 val expectedBox1X = (rootSize - boxSize) * box1Bias
1872                 val expectedBox1Y = (rootSize * 0.5f) - (boxSize * 0.5f)
1873                 assertEquals(Offset(expectedBox1X, expectedBox1Y).round(), box1Position)
1874 
1875                 val expectedBox1End = expectedBox1X + boxSize
1876                 val expectedBox2X =
1877                     (rootSize - expectedBox1End - boxSize) * (1f - box2Bias) + expectedBox1End
1878                 assertEquals(Offset(expectedBox2X, expectedBox1Y + boxSize).round(), box2Position)
1879             }
1880         }
1881 
1882     @Test
1883     fun testContentRecomposition_withConstraintSet() =
1884         with(rule.density) {
1885             var constraintLayoutCompCount = 0
1886 
1887             val baseWidth = 10
1888             val box0WidthMultiplier = mutableStateOf(2)
1889             val boxHeight = 30
1890             rule.setContent {
1891                 ++constraintLayoutCompCount
1892                 ConstraintLayout(
1893                     constraintSet =
1894                         ConstraintSet {
1895                             val (box0, box1) = createRefsFor("box0", "box1")
1896                             constrain(box0) {
1897                                 // previously, preferredWrapContent would fail if only the content
1898                                 // recomposed
1899                                 width = Dimension.preferredWrapContent
1900 
1901                                 start.linkTo(parent.start)
1902                                 end.linkTo(parent.end)
1903                                 horizontalBias = 0f
1904 
1905                                 top.linkTo(parent.top)
1906                             }
1907                             constrain(box1) {
1908                                 width = Dimension.fillToConstraints
1909                                 height = Dimension.wrapContent
1910                                 start.linkTo(box0.start)
1911                                 end.linkTo(box0.end)
1912                                 horizontalBias = 0f
1913 
1914                                 top.linkTo(box0.bottom)
1915                             }
1916                         }
1917                 ) {
1918                     Box(
1919                         Modifier.height(boxHeight.toDp())
1920                             .width((baseWidth * box0WidthMultiplier.value).toDp())
1921                             .layoutTestId("box0")
1922                             .background(Color.Red)
1923                     )
1924                     Box(
1925                         Modifier.height(boxHeight.toDp())
1926                             .layoutTestId("box1")
1927                             .background(Color.Blue)
1928                     )
1929                 }
1930             }
1931             rule.waitForIdle()
1932 
1933             rule.onNodeWithTag("box0").apply {
1934                 assertPositionInRootIsEqualTo(0.dp, 0.dp)
1935                 assertWidthIsEqualTo(20.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
1936             }
1937             rule.onNodeWithTag("box1").apply {
1938                 assertPositionInRootIsEqualTo(0.dp, boxHeight.toDp())
1939                 assertWidthIsEqualTo(20.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
1940             }
1941 
1942             box0WidthMultiplier.value = 3
1943             rule.waitForIdle()
1944 
1945             rule.onNodeWithTag("box0").apply {
1946                 assertPositionInRootIsEqualTo(0.dp, 0.dp)
1947                 assertWidthIsEqualTo(30.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
1948             }
1949             rule.onNodeWithTag("box1").apply {
1950                 assertPositionInRootIsEqualTo(0.dp, boxHeight.toDp())
1951                 assertWidthIsEqualTo(30.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
1952             }
1953 
1954             box0WidthMultiplier.value = 1
1955             rule.waitForIdle()
1956 
1957             rule.onNodeWithTag("box0").apply {
1958                 assertPositionInRootIsEqualTo(0.dp, 0.dp)
1959                 assertWidthIsEqualTo(10.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
1960             }
1961             rule.onNodeWithTag("box1").apply {
1962                 assertPositionInRootIsEqualTo(0.dp, boxHeight.toDp())
1963                 assertWidthIsEqualTo(10.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
1964             }
1965 
1966             assertEquals(1, constraintLayoutCompCount)
1967         }
1968 
1969     @Test
1970     fun testContentRecomposition_withInlineModifier() =
1971         with(rule.density) {
1972             var constraintLayoutCompCount = 0
1973 
1974             val baseWidth = 10
1975             val box0WidthMultiplier = mutableStateOf(2)
1976             val boxHeight = 30
1977             rule.setContent {
1978                 ++constraintLayoutCompCount
1979                 ConstraintLayout {
1980                     val (box0, box1) = createRefs()
1981                     Box(
1982                         Modifier.height(boxHeight.toDp())
1983                             .width((baseWidth * box0WidthMultiplier.value).toDp())
1984                             .constrainAs(box0) {
1985                                 // previously, preferredWrapContent would fail if only the content
1986                                 // recomposed
1987                                 width = Dimension.preferredWrapContent
1988 
1989                                 start.linkTo(parent.start)
1990                                 end.linkTo(parent.end)
1991                                 horizontalBias = 0f
1992 
1993                                 top.linkTo(parent.top)
1994                             }
1995                             .testTag("box0")
1996                             .background(Color.Red)
1997                     )
1998                     Box(
1999                         Modifier.height(boxHeight.toDp())
2000                             .constrainAs(box1) {
2001                                 width = Dimension.fillToConstraints
2002                                 height = Dimension.wrapContent
2003                                 start.linkTo(box0.start)
2004                                 end.linkTo(box0.end)
2005                                 horizontalBias = 0f
2006 
2007                                 top.linkTo(box0.bottom)
2008                             }
2009                             .testTag("box1")
2010                             .background(Color.Blue)
2011                     )
2012                 }
2013             }
2014             rule.waitForIdle()
2015 
2016             rule.onNodeWithTag("box0").apply {
2017                 assertPositionInRootIsEqualTo(0.dp, 0.dp)
2018                 assertWidthIsEqualTo(20.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
2019             }
2020             rule.onNodeWithTag("box1").apply {
2021                 assertPositionInRootIsEqualTo(0.dp, boxHeight.toDp())
2022                 assertWidthIsEqualTo(20.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
2023             }
2024 
2025             box0WidthMultiplier.value = 3
2026             rule.waitForIdle()
2027 
2028             rule.onNodeWithTag("box0").apply {
2029                 assertPositionInRootIsEqualTo(0.dp, 0.dp)
2030                 assertWidthIsEqualTo(30.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
2031             }
2032             rule.onNodeWithTag("box1").apply {
2033                 assertPositionInRootIsEqualTo(0.dp, boxHeight.toDp())
2034                 assertWidthIsEqualTo(30.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
2035             }
2036 
2037             box0WidthMultiplier.value = 1
2038             rule.waitForIdle()
2039 
2040             rule.onNodeWithTag("box0").apply {
2041                 assertPositionInRootIsEqualTo(0.dp, 0.dp)
2042                 assertWidthIsEqualTo(10.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
2043             }
2044             rule.onNodeWithTag("box1").apply {
2045                 assertPositionInRootIsEqualTo(0.dp, boxHeight.toDp())
2046                 assertWidthIsEqualTo(10.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
2047             }
2048 
2049             assertEquals(1, constraintLayoutCompCount)
2050         }
2051 
2052     @Test
2053     fun testBaselineConstraints() =
2054         with(rule.density) {
2055             fun Modifier.withBaseline() =
2056                 this.layout { measurable, constraints ->
2057                     val placeable = measurable.measure(constraints)
2058                     val halfHeight = (placeable.height / 2f).roundToInt()
2059                     layout(
2060                         width = placeable.width,
2061                         height = placeable.height,
2062                         alignmentLines = mapOf(FirstBaseline to halfHeight)
2063                     ) {
2064                         placeable.place(0, 0)
2065                     }
2066                 }
2067 
2068             val boxSize = 10
2069             val box1Margin = 13
2070             val box2Margin = -7f
2071 
2072             var box1Position = IntOffset.Zero
2073             var box2Position = IntOffset.Zero
2074             rule.setContent {
2075                 ConstraintLayout {
2076                     val (box1, box2) = createRefs()
2077                     Box(
2078                         Modifier.size(boxSize.toDp())
2079                             .withBaseline()
2080                             .constrainAs(box1) {
2081                                 baseline.linkTo(parent.top, box1Margin.toDp())
2082                                 start.linkTo(parent.start)
2083                             }
2084                             .onGloballyPositioned { box1Position = it.positionInRoot().round() }
2085                     )
2086                     Box(
2087                         Modifier.size(boxSize.toDp())
2088                             .withBaseline()
2089                             .constrainAs(box2) { top.linkTo(box1.baseline, box2Margin.toDp()) }
2090                             .onGloballyPositioned { box2Position = it.positionInRoot().round() }
2091                     )
2092                 }
2093             }
2094             val expectedBox1Y = box1Margin - (boxSize * 0.5f).roundToInt()
2095             val expectedBox2Y = expectedBox1Y + box2Margin + (boxSize * 0.5f).toInt()
2096             rule.runOnIdle {
2097                 assertEquals(IntOffset(0, expectedBox1Y), box1Position)
2098                 assertEquals(IntOffset(0, expectedBox2Y.roundToInt()), box2Position)
2099             }
2100         }
2101 
2102     @Test
2103     fun testConstraintLayout_withParentIntrinsics() =
2104         with(rule.density) {
2105             val rootBoxWidth = 200
2106             val box1Size = 40
2107             val box2Size = 70
2108 
2109             var rootSize = IntSize.Zero
2110             var clSize = IntSize.Zero
2111             var box1Position = IntOffset.Zero
2112             var box2Position = IntOffset.Zero
2113 
2114             rule.setContent {
2115                 Box(
2116                     modifier =
2117                         Modifier.width(rootBoxWidth.toDp())
2118                             .height(IntrinsicSize.Max)
2119                             .background(Color.LightGray)
2120                             .onGloballyPositioned { rootSize = it.size }
2121                 ) {
2122                     ConstraintLayout(
2123                         modifier =
2124                             Modifier.fillMaxWidth()
2125                                 .wrapContentHeight()
2126                                 .background(Color.Yellow)
2127                                 .onGloballyPositioned { clSize = it.size }
2128                     ) {
2129                         val (one, two) = createRefs()
2130                         val horChain =
2131                             createHorizontalChain(one, two, chainStyle = ChainStyle.Packed(0f))
2132                         constrain(horChain) {
2133                             start.linkTo(parent.start)
2134                             end.linkTo(parent.end)
2135                         }
2136                         Box(
2137                             Modifier.size(box1Size.toDp())
2138                                 .background(Color.Green)
2139                                 .constrainAs(one) {
2140                                     top.linkTo(parent.top)
2141                                     bottom.linkTo(parent.bottom)
2142                                 }
2143                                 .onGloballyPositioned { box1Position = it.positionInRoot().round() }
2144                         )
2145                         Box(
2146                             Modifier.size(box2Size.toDp())
2147                                 .background(Color.Red)
2148                                 .constrainAs(two) {
2149                                     width = Dimension.preferredWrapContent
2150                                     top.linkTo(parent.top)
2151                                     bottom.linkTo(parent.bottom)
2152                                 }
2153                                 .onGloballyPositioned { box2Position = it.positionInRoot().round() }
2154                         )
2155                     }
2156                 }
2157             }
2158 
2159             val expectedSize = IntSize(rootBoxWidth, box2Size)
2160             val expectedBox1Y = ((box2Size / 2f) - (box1Size / 2f)).roundToInt()
2161             rule.runOnIdle {
2162                 assertEquals(expectedSize, rootSize)
2163                 assertEquals(expectedSize, clSize)
2164                 assertEquals(IntOffset(0, expectedBox1Y), box1Position)
2165                 assertEquals(IntOffset(box1Size, 0), box2Position)
2166             }
2167         }
2168 
2169     @Test
2170     fun testTranslationXY_withDsl() =
2171         with(rule.density) {
2172             val rootSizePx = 100
2173             val boxSizePx = 10
2174             val translationXPx = 7
2175             val translationYPx = 9
2176 
2177             var position = IntOffset.Zero
2178 
2179             rule.setContent {
2180                 ConstraintLayout(Modifier.size(rootSizePx.toDp())) {
2181                     val boxRef = createRef()
2182                     Box(
2183                         Modifier.constrainAs(boxRef) {
2184                                 width = boxSizePx.toDp().asDimension()
2185                                 height = boxSizePx.toDp().asDimension()
2186                                 centerTo(parent)
2187 
2188                                 translationX = translationXPx.toDp()
2189                                 translationY = translationYPx.toDp()
2190                             }
2191                             .onPlaced {
2192                                 // TODO: Figure out a way to test `translationZ` i.e.:
2193                                 // `shadowElevation`
2194                                 position = it.boundsInParent().topLeft.round()
2195                             }
2196                     )
2197                 }
2198             }
2199 
2200             rule.runOnIdle {
2201                 assertEquals(
2202                     Offset(
2203                             (rootSizePx - boxSizePx) / 2f + translationXPx,
2204                             (rootSizePx - boxSizePx) / 2f + translationYPx,
2205                         )
2206                         .round(),
2207                     position
2208                 )
2209             }
2210         }
2211 
2212     @Test
2213     fun testTranslationXY_withJson() =
2214         with(rule.density) {
2215             val rootSizePx = 100
2216             val boxSizePx = 10
2217             val translationXPx = 7
2218             val translationYPx = 9
2219 
2220             var position = IntOffset.Zero
2221 
2222             rule.setContent {
2223                 ConstraintLayout(
2224                     constraintSet =
2225                         ConstraintSet(
2226                             """
2227                     {
2228                       box: {
2229                         width: ${boxSizePx.toDp().value},
2230                         height: ${boxSizePx.toDp().value},
2231                         center: 'parent',
2232                         translationX: ${translationXPx.toDp().value},
2233                         translationY: ${translationYPx.toDp().value}
2234                       }
2235                     }
2236                 """
2237                                 .trimIndent()
2238                         ),
2239                     Modifier.size(rootSizePx.toDp())
2240                 ) {
2241                     Box(
2242                         Modifier.layoutId("box").onPlaced {
2243                             // TODO: Figure out a way to test `translationZ` i.e.: `shadowElevation`
2244                             position = it.boundsInParent().topLeft.round()
2245                         }
2246                     )
2247                 }
2248             }
2249 
2250             rule.runOnIdle {
2251                 assertEquals(
2252                     Offset(
2253                             (rootSizePx - boxSizePx) / 2f + translationXPx,
2254                             (rootSizePx - boxSizePx) / 2f + translationYPx,
2255                         )
2256                         .round(),
2257                     position
2258                 )
2259             }
2260         }
2261 
2262     // Required for bitmap evaluation.
2263     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
2264     @Test
2265     fun testVisibility_withInlineDsl() =
2266         with(rule.density) {
2267             val rootSizePx = 100
2268             val boxSizePx = 10
2269             var box1Position = IntOffset.Zero
2270             val expectedInitialBox1Position =
2271                 Offset((rootSizePx + boxSizePx) / 2f, (rootSizePx - boxSizePx) / 2f).round()
2272 
2273             val boxVisibility = mutableStateOf(Visibility.Visible)
2274 
2275             rule.setContent {
2276                 ConstraintLayout(Modifier.size(rootSizePx.toDp())) {
2277                     val (box0, box1) = createRefs()
2278 
2279                     Box(
2280                         Modifier.testTag("box0")
2281                             .constrainAs(box0) {
2282                                 centerTo(parent)
2283                                 visibility = boxVisibility.value
2284                             }
2285                             .background(Color.Red)
2286                     ) {
2287                         Box(Modifier.size(boxSizePx.toDp()))
2288                     }
2289                     Box(
2290                         Modifier.testTag("box1")
2291                             .constrainAs(box1) {
2292                                 width = boxSizePx.toDp().asDimension()
2293                                 height = boxSizePx.toDp().asDimension()
2294 
2295                                 top.linkTo(box0.top)
2296                                 start.linkTo(box0.end)
2297                             }
2298                             .background(Color.Blue)
2299                             .onGloballyPositioned { box1Position = it.positionInParent().round() }
2300                     )
2301                 }
2302             }
2303             rule.waitForIdle()
2304 
2305             var color = rule.onNodeWithTag("box0").captureToImage().asAndroidBitmap().getColor(5, 5)
2306             assertEquals(Color.Red.toArgb(), color.toArgb())
2307             assertEquals(expectedInitialBox1Position, box1Position)
2308 
2309             boxVisibility.value = Visibility.Invisible
2310             rule.waitForIdle()
2311 
2312             color = rule.onNodeWithTag("box0").captureToImage().asAndroidBitmap().getColor(5, 5)
2313             assertNotEquals(Color.Red.toArgb(), color.toArgb())
2314             assertEquals(expectedInitialBox1Position, box1Position)
2315 
2316             boxVisibility.value = Visibility.Gone
2317             rule.waitForIdle()
2318 
2319             // Dp.Unspecified since Gone Composables are not placed
2320             rule.onNodeWithTag("box0").assertWidthIsEqualTo(Dp.Unspecified)
2321             rule.onNodeWithTag("box0").assertHeightIsEqualTo(Dp.Unspecified)
2322             assertEquals(Offset(rootSizePx / 2f, rootSizePx / 2f).round(), box1Position)
2323         }
2324 
2325     @Test
2326     fun testAnimateChanges_withInlineDsl() =
2327         with(rule.density) {
2328             val durationMs = 200
2329             val rootSizePx = 100
2330             val boxSizePx = 20
2331             val expectedEndPosition = IntOffset(rootSizePx - boxSizePx, rootSizePx - boxSizePx)
2332             var box0Position = IntOffset.Zero
2333             val atTopLeftCorner = mutableStateOf(true)
2334 
2335             rule.setContent {
2336                 ConstraintLayout(
2337                     modifier = Modifier.size(rootSizePx.toDp()),
2338                     animateChangesSpec = tween(durationMs)
2339                 ) {
2340                     val boxRef = createRef()
2341                     Box(
2342                         Modifier.background(Color.Red)
2343                             .constrainAs(boxRef) {
2344                                 width = boxSizePx.toDp().asDimension()
2345                                 height = boxSizePx.toDp().asDimension()
2346                                 if (atTopLeftCorner.value) {
2347                                     top.linkTo(parent.top)
2348                                     start.linkTo(parent.start)
2349                                 } else {
2350                                     bottom.linkTo(parent.bottom)
2351                                     end.linkTo(parent.end)
2352                                 }
2353                             }
2354                             .onGloballyPositioned { box0Position = it.positionInParent().round() }
2355                     )
2356                 }
2357             }
2358             rule.waitForIdle()
2359             assertEquals(IntOffset.Zero, box0Position)
2360 
2361             rule.mainClock.autoAdvance = false
2362             atTopLeftCorner.value = false
2363 
2364             rule.mainClock.advanceTimeBy(durationMs / 2L)
2365             rule.waitForIdle()
2366             assertTrue(box0Position.x > 0 && box0Position.y > 0)
2367             assertTrue(
2368                 box0Position.x < expectedEndPosition.x && box0Position.y < expectedEndPosition.y
2369             )
2370 
2371             rule.mainClock.autoAdvance = true
2372             rule.waitForIdle()
2373             assertEquals(expectedEndPosition, box0Position)
2374         }
2375 
2376     @Test
2377     fun testEmptyConstraintLayoutSize() =
2378         with(rule.density) {
2379             val rootSizePx = 200f
2380 
2381             // Mutable state to trigger invalidations. By default simply passes original
2382             // constraints.
2383             // But we'll use this to test empty ConstraintLayout under different constraints
2384             var transformConstraints: ((Constraints) -> Constraints) by
2385                 mutableStateOf({ constraints -> constraints })
2386 
2387             // To capture measured ConstraintLayout size
2388             var layoutSize = IntSize(-1, -1)
2389 
2390             rule.setContent {
2391                 Column(Modifier.size(rootSizePx.toDp()).verticalScroll(rememberScrollState())) {
2392                     ConstraintLayout(
2393                         modifier =
2394                             Modifier.layout { measurable, constraints ->
2395                                     // Measure policy to test ConstraintLayout under different
2396                                     // constraints
2397                                     val placeable =
2398                                         measurable.measure(transformConstraints(constraints))
2399                                     layout(placeable.width, placeable.height) {
2400                                         placeable.place(0, 0)
2401                                     }
2402                                 }
2403                                 .onGloballyPositioned { layoutSize = it.size }
2404                     ) {
2405                         // Empty content
2406                     }
2407                 }
2408             }
2409             // For this case, the default behavior should be a ConstraintLayout of size 0x0
2410             rule.waitForIdle()
2411             assertEquals(IntSize.Zero, layoutSize)
2412 
2413             // Test with min constraints
2414             transformConstraints = { constraints ->
2415                 // Demonstrate that vertical scroll constraints propagate
2416                 assert(constraints.maxHeight == Constraints.Infinity)
2417                 constraints.copy(minWidth = 123, minHeight = 321)
2418             }
2419             rule.waitForIdle()
2420 
2421             // Minimum size is preferred for empty layouts. Should not crash :)
2422             assertEquals(IntSize(width = 123, height = 321), layoutSize)
2423 
2424             // Transform to an equivalent of fillMaxSize(), which fills bounded constraints only
2425             transformConstraints = { constraints ->
2426                 val minWidth: Int
2427                 val maxWidth: Int
2428                 val minHeight: Int
2429                 val maxHeight: Int
2430 
2431                 if (constraints.hasBoundedWidth) {
2432                     minWidth = constraints.maxWidth
2433                     maxWidth = constraints.maxWidth
2434                 } else {
2435                     minWidth = constraints.minWidth
2436                     maxWidth = constraints.maxWidth
2437                 }
2438                 if (constraints.hasBoundedHeight) {
2439                     minHeight = constraints.maxHeight
2440                     maxHeight = constraints.maxHeight
2441                 } else {
2442                     minHeight = constraints.minHeight
2443                     maxHeight = constraints.maxHeight
2444                 }
2445                 Constraints(
2446                     minWidth = minWidth,
2447                     maxWidth = maxWidth,
2448                     minHeight = minHeight,
2449                     maxHeight = maxHeight
2450                 )
2451             }
2452             rule.waitForIdle()
2453 
2454             // Vertical is infinity, fillMaxSize behavior should pin it to minimum height (Zero)
2455             assertEquals(IntSize(rootSizePx.fastRoundToInt(), 0), layoutSize)
2456         }
2457 
2458     @Test
2459     fun testToggleVisibilityWithFillConstraintsWidth() =
2460         with(rule.density) {
2461             val rootSizePx = 100f
2462 
2463             var toggleVisibility by mutableStateOf(false)
2464 
2465             rule.setContent {
2466                 // Regression test, modify only dimensions if necessary
2467                 ConstraintLayout(modifier = Modifier.size(rootSizePx.toDp())) {
2468                     val (titleRef, detailRef) = createRefs()
2469                     Box(
2470                         modifier =
2471                             Modifier.background(Color.Cyan).testTag("box1").constrainAs(detailRef) {
2472                                 centerHorizontallyTo(parent)
2473 
2474                                 width = Dimension.fillToConstraints
2475                                 height = rootSizePx.toDp().asDimension()
2476 
2477                                 visibility =
2478                                     if (!toggleVisibility) Visibility.Gone else Visibility.Visible
2479                             }
2480                     )
2481                     Box(
2482                         modifier =
2483                             Modifier.background(Color.Red).testTag("box2").constrainAs(titleRef) {
2484                                 centerHorizontallyTo(parent)
2485 
2486                                 width = Dimension.fillToConstraints
2487                                 height = rootSizePx.toDp().asDimension()
2488 
2489                                 visibility =
2490                                     if (toggleVisibility) Visibility.Gone else Visibility.Visible
2491                             }
2492                     )
2493                 }
2494             }
2495             rule.waitForIdle()
2496 
2497             rule.onNodeWithTag("box1").apply {
2498                 assertWidthIsEqualTo(Dp.Unspecified)
2499                 assertHeightIsEqualTo(Dp.Unspecified)
2500             }
2501             rule.onNodeWithTag("box2").apply {
2502                 assertWidthIsEqualTo(rootSizePx.toDp())
2503                 assertHeightIsEqualTo(rootSizePx.toDp())
2504             }
2505 
2506             toggleVisibility = !toggleVisibility
2507             rule.waitForIdle()
2508 
2509             rule.onNodeWithTag("box1").apply {
2510                 assertWidthIsEqualTo(rootSizePx.toDp())
2511                 assertHeightIsEqualTo(rootSizePx.toDp())
2512             }
2513             rule.onNodeWithTag("box2").apply {
2514                 assertWidthIsEqualTo(Dp.Unspecified)
2515                 assertHeightIsEqualTo(Dp.Unspecified)
2516             }
2517             Unit // Test expects to return Unit
2518         }
2519 
2520     @Test
2521     fun testToggleVisibilityWithFillConstraintsWidth_underLookahead() =
2522         with(rule.density) {
2523             val rootSizePx = 100f
2524 
2525             var toggleVisibility by mutableStateOf(false)
2526 
2527             rule.setContent {
2528                 LookaheadScope {
2529                     // Regression test, modify only dimensions if necessary
2530                     ConstraintLayout(modifier = Modifier.size(rootSizePx.toDp())) {
2531                         val (titleRef, detailRef) = createRefs()
2532                         Box(
2533                             modifier =
2534                                 Modifier.background(Color.Cyan).testTag("box1").constrainAs(
2535                                     detailRef
2536                                 ) {
2537                                     centerHorizontallyTo(parent)
2538 
2539                                     width = Dimension.fillToConstraints
2540                                     height = rootSizePx.toDp().asDimension()
2541 
2542                                     visibility =
2543                                         if (!toggleVisibility) Visibility.Gone
2544                                         else Visibility.Visible
2545                                 }
2546                         )
2547                         Box(
2548                             modifier =
2549                                 Modifier.background(Color.Red).testTag("box2").constrainAs(
2550                                     titleRef
2551                                 ) {
2552                                     centerHorizontallyTo(parent)
2553 
2554                                     width = Dimension.fillToConstraints
2555                                     height = rootSizePx.toDp().asDimension()
2556 
2557                                     visibility =
2558                                         if (toggleVisibility) Visibility.Gone
2559                                         else Visibility.Visible
2560                                 }
2561                         )
2562                     }
2563                 }
2564             }
2565             rule.waitForIdle()
2566 
2567             rule.onNodeWithTag("box1").apply {
2568                 assertWidthIsEqualTo(Dp.Unspecified)
2569                 assertHeightIsEqualTo(Dp.Unspecified)
2570             }
2571             rule.onNodeWithTag("box2").apply {
2572                 assertWidthIsEqualTo(rootSizePx.toDp())
2573                 assertHeightIsEqualTo(rootSizePx.toDp())
2574             }
2575 
2576             toggleVisibility = !toggleVisibility
2577             rule.waitForIdle()
2578 
2579             rule.onNodeWithTag("box1").apply {
2580                 assertWidthIsEqualTo(rootSizePx.toDp())
2581                 assertHeightIsEqualTo(rootSizePx.toDp())
2582             }
2583             rule.onNodeWithTag("box2").apply {
2584                 assertWidthIsEqualTo(Dp.Unspecified)
2585                 assertHeightIsEqualTo(Dp.Unspecified)
2586             }
2587             Unit // Test expects to return Unit
2588         }
2589 
2590     /**
2591      * Provides a list constraints combination for horizontal anchors: `start`, `end`,
2592      * `absoluteLeft`, `absoluteRight`.
2593      */
2594     private fun listAnchors(box: ConstrainedLayoutReference): List<ConstrainScope.() -> Unit> =
2595         listOf(
2596             { start.linkTo(box.start) },
2597             { absoluteLeft.linkTo(box.start) },
2598             { start.linkTo(box.absoluteLeft) },
2599             { absoluteLeft.linkTo(box.absoluteLeft) },
2600             { end.linkTo(box.start) },
2601             { absoluteRight.linkTo(box.start) },
2602             { end.linkTo(box.absoluteLeft) },
2603             { absoluteRight.linkTo(box.absoluteLeft) },
2604             { start.linkTo(box.end) },
2605             { absoluteLeft.linkTo(box.end) },
2606             { start.linkTo(box.absoluteRight) },
2607             { absoluteLeft.linkTo(box.absoluteRight) },
2608             { end.linkTo(box.end) },
2609             { absoluteRight.linkTo(box.end) },
2610             { end.linkTo(box.absoluteRight) },
2611             { absoluteRight.linkTo(box.absoluteRight) },
2612         )
2613 
2614     private fun getJsonAnchorsContent(guidelineOffset: Float): String =
2615         // language=json5
2616         """
2617             {
2618               g1: { type: 'vGuideline', left: $guidelineOffset },
2619               box: { left: ['g1', 'left', 0] },
2620               box0: { start: ['box','start',0] },
2621               box1: { left: ['box','start',0] },
2622               box2: { start: ['box','left',0] },
2623               box3: { left: ['box','left',0] },
2624               box4: { end: ['box','start',0] },
2625               box5: { right: ['box','start',0] },
2626               box6: { end: ['box','left',0] },
2627               box7: { right: ['box','left',0] },
2628               box8: { start: ['box','end',0] },
2629               box9: { left: ['box','end',0] },
2630               box10: { start: ['box','right',0] },
2631               box11: { left: ['box','right',0] },
2632               box12: { end: ['box','end',0] },
2633               box13: { right: ['box','end',0] },
2634               box14: { end: ['box','right',0] },
2635               box15: { right: ['box','right',0] }
2636             }
2637         """
2638             .trimIndent()
2639 
2640     private fun assertAnchorsLtrPositions(position: Array<Float>) {
2641         rule.runOnIdle {
2642             assertEquals(16, position.size)
2643             assertEquals(50f, position[0])
2644             assertEquals(50f, position[1])
2645             assertEquals(50f, position[2])
2646             assertEquals(50f, position[3])
2647             assertEquals(49f, position[4])
2648             assertEquals(49f, position[5])
2649             assertEquals(49f, position[6])
2650             assertEquals(49f, position[7])
2651             assertEquals(51f, position[8])
2652             assertEquals(51f, position[9])
2653             assertEquals(51f, position[10])
2654             assertEquals(51f, position[11])
2655             assertEquals(50f, position[12])
2656             assertEquals(50f, position[13])
2657             assertEquals(50f, position[14])
2658             assertEquals(50f, position[15])
2659         }
2660     }
2661 
2662     private fun assertAnchorsRtlPositions(position: Array<Float>) {
2663         rule.runOnIdle {
2664             assertEquals(16, position.size)
2665             assertEquals(50f, position[0])
2666             assertEquals(51f, position[1])
2667             assertEquals(49f, position[2])
2668             assertEquals(50f, position[3])
2669             assertEquals(51f, position[4])
2670             assertEquals(50f, position[5])
2671             assertEquals(50f, position[6])
2672             assertEquals(49f, position[7])
2673             assertEquals(49f, position[8])
2674             assertEquals(50f, position[9])
2675             assertEquals(50f, position[10])
2676             assertEquals(51f, position[11])
2677             assertEquals(50f, position[12])
2678             assertEquals(49f, position[13])
2679             assertEquals(51f, position[14])
2680             assertEquals(50f, position[15])
2681         }
2682     }
2683 
2684     private fun getJsonGuidelinesContent(guidelineOffset: Float): String =
2685         // language=json5
2686         """
2687             {
2688               g0: { type: 'vGuideline', start: $guidelineOffset },
2689               g1: { type: 'vGuideline', left: $guidelineOffset },
2690               g2: { type: 'vGuideline', end: $guidelineOffset },
2691               g3: { type: 'vGuideline', right: $guidelineOffset },
2692               g4: { type: 'vGuideline', percent: ["start", 0.25] },
2693               g5: { type: 'vGuideline', percent: ["left", 0.25] },
2694               g6: { type: 'vGuideline', percent: ["end", 0.25] },
2695               g7: { type: 'vGuideline', percent: ["right", 0.25] },
2696               box0: { left: ['g0', 'start', 0] },
2697               box1: { left: ['g1', 'start', 0] },
2698               box2: { left: ['g2', 'start', 0] },
2699               box3: { left: ['g3', 'start', 0] },
2700               box4: { left: ['g4', 'start', 0] },
2701               box5: { left: ['g5', 'start', 0] },
2702               box6: { left: ['g6', 'start', 0] },
2703               box7: { left: ['g7', 'start', 0] }
2704             }
2705         """
2706             .trimIndent()
2707 
2708     private fun assertGuidelinesLtrPositions(position: Array<Float>) {
2709         rule.runOnIdle {
2710             assertEquals(8, position.size)
2711             assertEquals(50f, position[0])
2712             assertEquals(50f, position[1])
2713             assertEquals(150f, position[2])
2714             assertEquals(150f, position[3])
2715             assertEquals(50f, position[4])
2716             assertEquals(50f, position[5])
2717             assertEquals(150f, position[6])
2718             assertEquals(150f, position[7])
2719         }
2720     }
2721 
2722     private fun assertGuidelinesRtlPositions(position: Array<Float>) {
2723         rule.runOnIdle {
2724             assertEquals(8, position.size)
2725             assertEquals(150f, position[0])
2726             assertEquals(50f, position[1])
2727             assertEquals(50f, position[2])
2728             assertEquals(150f, position[3])
2729             assertEquals(150f, position[4])
2730             assertEquals(50f, position[5])
2731             assertEquals(50f, position[6])
2732             assertEquals(150f, position[7])
2733         }
2734     }
2735 
2736     private fun getJsonBarriersContent(guidelineOffset: Float): String =
2737         // language=json5
2738         """
2739             {
2740               g0: { type: 'vGuideline', left: $guidelineOffset },
2741               g1: { type: 'vGuideline', right: $guidelineOffset },
2742 
2743               boxA: { left: ['g0', 'start', 0] },
2744               boxB: { left: ['g1', 'start', 0] },
2745 
2746               b0: { type: 'barrier', direction: 'start', contains: ['boxA','boxB'] },
2747               b1: { type: 'barrier', direction: 'left', contains: ['boxA','boxB'] },
2748               b2: { type: 'barrier', direction: 'end', contains: ['boxA','boxB'] },
2749               b3: { type: 'barrier', direction: 'right', contains: ['boxA','boxB'] },
2750 
2751               box0: { left: ['b0', 'start', 0] },
2752               box1: { left: ['b1', 'start', 0] },
2753               box2: { left: ['b2', 'start', 0] },
2754               box3: { left: ['b3', 'start', 0] },
2755             }
2756         """
2757             .trimIndent()
2758 
2759     private fun assertBarriersLtrPositions(position: Array<Float>) {
2760         rule.runOnIdle {
2761             assertEquals(4, position.size)
2762             assertEquals(50f, position[0])
2763             assertEquals(50f, position[1])
2764             assertEquals(151f, position[2])
2765             assertEquals(151f, position[3])
2766         }
2767     }
2768 
2769     private fun assertBarriersRtlPositions(position: Array<Float>) {
2770         rule.runOnIdle {
2771             assertEquals(4, position.size)
2772             assertEquals(151f, position[0])
2773             assertEquals(50f, position[1])
2774             assertEquals(50f, position[2])
2775             assertEquals(151f, position[3])
2776         }
2777     }
2778 }
2779