1 /*
<lambda>null2  * Copyright (C) 2022 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 androidx.compose.foundation.background
20 import androidx.compose.foundation.layout.Box
21 import androidx.compose.foundation.layout.size
22 import androidx.compose.runtime.Composable
23 import androidx.compose.ui.Modifier
24 import androidx.compose.ui.graphics.Color
25 import androidx.compose.ui.layout.layoutId
26 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
27 import androidx.compose.ui.platform.testTag
28 import androidx.compose.ui.test.assertPositionInRootIsEqualTo
29 import androidx.compose.ui.test.junit4.createComposeRule
30 import androidx.compose.ui.test.onNodeWithTag
31 import androidx.compose.ui.unit.Dp
32 import androidx.compose.ui.unit.dp
33 import androidx.test.ext.junit.runners.AndroidJUnit4
34 import androidx.test.filters.MediumTest
35 import org.junit.After
36 import org.junit.Before
37 import org.junit.Rule
38 import org.junit.Test
39 import org.junit.runner.RunWith
40 
41 /** Tests for the Grid Helper */
42 @MediumTest
43 @RunWith(AndroidJUnit4::class)
44 class GridTest {
45     @get:Rule val rule = createComposeRule()
46 
47     @Before
48     fun setup() {
49         isDebugInspectorInfoEnabled = true
50     }
51 
52     @After
53     fun tearDown() {
54         isDebugInspectorInfoEnabled = false
55     }
56 
57     @Test
58     fun testTwoByTwo() {
59         val rootSize = 200.dp
60         val boxesCount = 4
61         val rows = 2
62         val columns = 2
63         rule.setContent {
64             gridComposableTest(
65                 modifier = Modifier.size(rootSize),
66                 width = "'parent'",
67                 height = "'parent'",
68                 boxesCount = boxesCount,
69                 orientation = 0,
70                 rows = rows,
71                 columns = columns,
72                 hGap = 0,
73                 vGap = 0,
74                 spans = "''",
75                 skips = "''",
76                 rowWeights = "''",
77                 columnWeights = "''",
78                 flags = "''"
79             )
80         }
81         var leftX = 0.dp
82         var topY = 0.dp
83         var rightX: Dp
84         var bottomY: Dp
85 
86         // 10.dp is the size of a singular box
87         val gapSize = (rootSize - (10.dp * 2f)) / (columns * 2f)
88         rule.waitForIdle()
89         leftX += gapSize
90         topY += gapSize
91         rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(leftX, topY)
92         rightX = leftX + 10.dp + gapSize + gapSize
93         rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(rightX, topY)
94         bottomY = topY + 10.dp + gapSize + gapSize
95         rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(leftX, bottomY)
96         rule.onNodeWithTag("box3").assertPositionInRootIsEqualTo(rightX, bottomY)
97     }
98 
99     @Test
100     fun testOrientation() {
101         val rootSize = 200.dp
102         val boxesCount = 4
103         val rows = 2
104         val columns = 2
105         rule.setContent {
106             gridComposableTest(
107                 modifier = Modifier.size(rootSize),
108                 width = "'parent'",
109                 height = "'parent'",
110                 boxesCount = boxesCount,
111                 orientation = 1,
112                 rows = rows,
113                 columns = columns,
114                 hGap = 0,
115                 vGap = 0,
116                 spans = "''",
117                 skips = "''",
118                 rowWeights = "''",
119                 columnWeights = "''",
120                 flags = "''"
121             )
122         }
123         var leftX = 0.dp
124         var topY = 0.dp
125         var rightX: Dp
126         var bottomY: Dp
127 
128         // 10.dp is the size of a singular box
129         val gapSize = (rootSize - (10.dp * 2f)) / (columns * 2f)
130         rule.waitForIdle()
131         leftX += gapSize
132         topY += gapSize
133         rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(leftX, topY)
134         rightX = leftX + 10.dp + gapSize + gapSize
135         rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(rightX, topY)
136         bottomY = topY + 10.dp + gapSize + gapSize
137         rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(leftX, bottomY)
138         rule.onNodeWithTag("box3").assertPositionInRootIsEqualTo(rightX, bottomY)
139     }
140 
141     @Test
142     fun testRows() {
143         val rootSize = 200.dp
144         val boxesCount = 4
145         val rows = 0
146         val columns = 1
147         rule.setContent {
148             gridComposableTest(
149                 modifier = Modifier.size(rootSize),
150                 width = "'parent'",
151                 height = "'parent'",
152                 boxesCount = boxesCount,
153                 orientation = 0,
154                 rows = rows,
155                 columns = columns,
156                 hGap = 0,
157                 vGap = 0,
158                 spans = "''",
159                 skips = "''",
160                 rowWeights = "''",
161                 columnWeights = "''",
162                 flags = "''"
163             )
164         }
165         var expectedX = 0.dp
166         var expectedY = 0.dp
167 
168         // 10.dp is the size of a singular box
169         val hGapSize = (rootSize - 10.dp) / 2f
170         val vGapSize = (rootSize - (10.dp * 4f)) / (boxesCount * 2f)
171         rule.waitForIdle()
172         expectedX += hGapSize
173         expectedY += vGapSize
174         rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(expectedX, expectedY)
175         expectedY += vGapSize + vGapSize + 10.dp
176         rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(expectedX, expectedY)
177         expectedY += vGapSize + vGapSize + 10.dp
178         rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(expectedX, expectedY)
179         expectedY += vGapSize + vGapSize + 10.dp
180         rule.onNodeWithTag("box3").assertPositionInRootIsEqualTo(expectedX, expectedY)
181     }
182 
183     @Test
184     fun testColumns() {
185         val rootSize = 200.dp
186         val boxesCount = 4
187         val rows = 1
188         val columns = 0
189         rule.setContent {
190             gridComposableTest(
191                 modifier = Modifier.size(rootSize),
192                 width = "'parent'",
193                 height = "'parent'",
194                 boxesCount = boxesCount,
195                 orientation = 0,
196                 rows = rows,
197                 columns = columns,
198                 hGap = 0,
199                 vGap = 0,
200                 spans = "''",
201                 skips = "''",
202                 rowWeights = "''",
203                 columnWeights = "''",
204                 flags = "''"
205             )
206         }
207         var expectedX = 0.dp
208         var expectedY = 0.dp
209 
210         // 10.dp is the size of a singular box
211         val hGapSize = (rootSize - (10.dp * 4f)) / (boxesCount * 2f)
212         val vGapSize = (rootSize - 10.dp) / 2f
213         rule.waitForIdle()
214         expectedX += hGapSize
215         expectedY += vGapSize
216         rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(expectedX, expectedY)
217         expectedX += hGapSize + hGapSize + 10.dp
218         rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(expectedX, expectedY)
219         expectedX += hGapSize + hGapSize + 10.dp
220         rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(expectedX, expectedY)
221         expectedX += hGapSize + hGapSize + 10.dp
222         rule.onNodeWithTag("box3").assertPositionInRootIsEqualTo(expectedX, expectedY)
223     }
224 
225     @Test
226     fun testSkips() {
227         val rootSize = 200.dp
228         val boxesCount = 3
229         val rows = 2
230         val columns = 2
231         rule.setContent {
232             gridComposableTest(
233                 modifier = Modifier.size(rootSize),
234                 width = "'parent'",
235                 height = "'parent'",
236                 boxesCount = boxesCount,
237                 orientation = 0,
238                 rows = rows,
239                 columns = columns,
240                 hGap = 0,
241                 vGap = 0,
242                 spans = "''",
243                 skips = "'0:1x1'",
244                 rowWeights = "''",
245                 columnWeights = "''",
246                 flags = "''"
247             )
248         }
249         var leftX = 0.dp
250         var topY = 0.dp
251         var rightX: Dp
252         var bottomY: Dp
253 
254         // 10.dp is the size of a singular box
255         val gapSize = (rootSize - (10.dp * 2f)) / (columns * 2f)
256         rule.waitForIdle()
257         leftX += gapSize
258         topY += gapSize
259         rightX = leftX + 10.dp + gapSize + gapSize
260         rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(rightX, topY)
261         bottomY = topY + 10.dp + gapSize + gapSize
262         rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(leftX, bottomY)
263         rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(rightX, bottomY)
264     }
265 
266     @Test
267     fun testReversedDirectionSkips() {
268         val rootSize = 200.dp
269         val boxesCount = 2
270         val rows = 2
271         val columns = 2
272         rule.setContent {
273             gridComposableTest(
274                 modifier = Modifier.size(rootSize),
275                 width = "'parent'",
276                 height = "'parent'",
277                 boxesCount = boxesCount,
278                 orientation = 0,
279                 rows = rows,
280                 columns = columns,
281                 hGap = 0,
282                 vGap = 0,
283                 spans = "''",
284                 skips = "'0:2x1'",
285                 rowWeights = "''",
286                 columnWeights = "''",
287                 flags = "'SubGridByColRow'"
288             )
289         }
290         var leftX = 0.dp
291         var topY = 0.dp
292         var rightX: Dp
293         var bottomY: Dp
294 
295         // 10.dp is the size of a singular box
296         val gapSize = (rootSize - (10.dp * 2f)) / (columns * 2f)
297         rule.waitForIdle()
298         leftX += gapSize
299         topY += gapSize
300         rightX = leftX + 10.dp + gapSize + gapSize
301         bottomY = topY + 10.dp + gapSize + gapSize
302         rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(leftX, bottomY)
303         rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(rightX, bottomY)
304     }
305 
306     @Test
307     fun testSpans() {
308         val rootSize = 200.dp
309         val boxesCount = 3
310         val rows = 2
311         val columns = 2
312         rule.setContent {
313             gridComposableTest(
314                 modifier = Modifier.size(rootSize),
315                 width = "'parent'",
316                 height = "'parent'",
317                 boxesCount = boxesCount,
318                 orientation = 0,
319                 rows = rows,
320                 columns = columns,
321                 hGap = 0,
322                 vGap = 0,
323                 spans = "'0:1x2'",
324                 skips = "''",
325                 rowWeights = "''",
326                 columnWeights = "''",
327                 flags = "''"
328             )
329         }
330         var leftX = 0.dp
331         var topY = 0.dp
332         var rightX: Dp
333         var bottomY: Dp
334 
335         // 10.dp is the size of a singular box
336         var spanLeft = (rootSize - 10.dp) / 2f
337         val gapSize = (rootSize - (10.dp * 2f)) / (columns * 2f)
338         rule.waitForIdle()
339         leftX += gapSize
340         topY += gapSize
341         rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(spanLeft, topY)
342         rightX = leftX + 10.dp + gapSize + gapSize
343         bottomY = topY + 10.dp + gapSize + gapSize
344         rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(leftX, bottomY)
345         rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(rightX, bottomY)
346     }
347 
348     @Test
349     fun testMultipleSpans() {
350         val rootSize = 200.dp
351         val boxesCount = 2
352         val rows = 2
353         val columns = 2
354         rule.setContent {
355             gridComposableTest(
356                 modifier = Modifier.size(rootSize),
357                 width = "'parent'",
358                 height = "'parent'",
359                 boxesCount = boxesCount,
360                 orientation = 0,
361                 rows = rows,
362                 columns = columns,
363                 hGap = 0,
364                 vGap = 0,
365                 spans = "'2:1x2,0:1x2'",
366                 skips = "''",
367                 rowWeights = "''",
368                 columnWeights = "''",
369                 flags = "'SpansRespectWidgetOrder'"
370             )
371         }
372         var topY = 0.dp
373         var bottomY: Dp
374 
375         // 10.dp is the size of a singular box
376         var spanLeft = (rootSize - 10.dp) / 2f
377         val gapSize = (rootSize - (10.dp * 2f)) / (columns * 2f)
378         rule.waitForIdle()
379         topY += gapSize
380         rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(spanLeft, topY)
381         bottomY = topY + 10.dp + gapSize + gapSize
382         rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(spanLeft, bottomY)
383     }
384 
385     @Test
386     fun testOrderFirstSpans() {
387         val rootSize = 200.dp
388         val boxesCount = 3
389         val rows = 2
390         val columns = 2
391         rule.setContent {
392             gridComposableTest(
393                 modifier = Modifier.size(rootSize),
394                 width = "'parent'",
395                 height = "'parent'",
396                 boxesCount = boxesCount,
397                 orientation = 0,
398                 rows = rows,
399                 columns = columns,
400                 hGap = 0,
401                 vGap = 0,
402                 spans = "'1:2x1'",
403                 skips = "''",
404                 rowWeights = "''",
405                 columnWeights = "''",
406                 flags = "'SpansRespectWidgetOrder'"
407             )
408         }
409         var leftX = 0.dp
410         var topY = 0.dp
411         var rightX: Dp
412         var bottomY: Dp
413 
414         // 10.dp is the size of a singular box
415         var spanTop = (rootSize - 10.dp) / 2f
416         val gapSize = (rootSize - (10.dp * 2f)) / (columns * 2f)
417         rule.waitForIdle()
418         leftX += gapSize
419         topY += gapSize
420         rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(leftX, topY)
421         rightX = leftX + 10.dp + gapSize + gapSize
422         rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(rightX, spanTop)
423         bottomY = topY + 10.dp + gapSize + gapSize
424         rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(leftX, bottomY)
425     }
426 
427     @Test
428     fun testReversedDirectionSpans() {
429         val rootSize = 200.dp
430         val boxesCount = 3
431         val rows = 2
432         val columns = 2
433         rule.setContent {
434             gridComposableTest(
435                 modifier = Modifier.size(rootSize),
436                 width = "'parent'",
437                 height = "'parent'",
438                 boxesCount = boxesCount,
439                 orientation = 0,
440                 rows = rows,
441                 columns = columns,
442                 hGap = 0,
443                 vGap = 0,
444                 spans = "'0:2x1'",
445                 skips = "''",
446                 rowWeights = "''",
447                 columnWeights = "''",
448                 flags = "'SubGridByColRow'"
449             )
450         }
451         var leftX = 0.dp
452         var topY = 0.dp
453         var rightX: Dp
454         var bottomY: Dp
455 
456         // 10.dp is the size of a singular box
457         var spanLeft = (rootSize - 10.dp) / 2f
458         val gapSize = (rootSize - (10.dp * 2f)) / (columns * 2f)
459         rule.waitForIdle()
460         leftX += gapSize
461         topY += gapSize
462         rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(spanLeft, topY)
463         rightX = leftX + 10.dp + gapSize + gapSize
464         bottomY = topY + 10.dp + gapSize + gapSize
465         rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(leftX, bottomY)
466         rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(rightX, bottomY)
467     }
468 
469     @Test
470     fun testRowWeights() {
471         val rootSize = 200.dp
472         val boxesCount = 2
473         val rows = 0
474         val columns = 1
475         rule.setContent {
476             gridComposableTest(
477                 modifier = Modifier.size(rootSize),
478                 width = "'parent'",
479                 height = "'parent'",
480                 boxesCount = boxesCount,
481                 orientation = 0,
482                 rows = rows,
483                 columns = columns,
484                 hGap = 0,
485                 vGap = 0,
486                 spans = "''",
487                 skips = "''",
488                 rowWeights = "'1,3'",
489                 columnWeights = "''",
490                 flags = "''"
491             )
492         }
493         var expectedLeft = (rootSize - 10.dp) / 2f
494         var expectedTop = 0.dp
495 
496         // 10.dp is the size of a singular box
497         // first box takes the 1/4 of the height
498         val firstGapSize = (rootSize / 4 - 10.dp) / 2
499         // second box takes the 3/4 of the height
500         val secondGapSize = ((rootSize * 3 / 4) - 10.dp) / 2
501         rule.waitForIdle()
502         expectedTop += firstGapSize
503         rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(expectedLeft, expectedTop)
504         expectedTop += 10.dp + firstGapSize + secondGapSize
505         rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(expectedLeft, expectedTop)
506     }
507 
508     @Test
509     fun testColumnWeights() {
510         val rootSize = 200.dp
511         val boxesCount = 2
512         val rows = 1
513         val columns = 0
514         rule.setContent {
515             gridComposableTest(
516                 modifier = Modifier.size(rootSize),
517                 width = "'parent'",
518                 height = "'parent'",
519                 boxesCount = boxesCount,
520                 orientation = 0,
521                 rows = rows,
522                 columns = columns,
523                 hGap = 0,
524                 vGap = 0,
525                 spans = "''",
526                 skips = "''",
527                 rowWeights = "''",
528                 columnWeights = "'1,3'",
529                 flags = "''"
530             )
531         }
532         var expectedLeft = 0.dp
533         var expectedTop = (rootSize - 10.dp) / 2f
534 
535         // 10.dp is the size of a singular box
536         // first box takes the 1/4 of the width
537         val firstGapSize = (rootSize / 4 - 10.dp) / 2
538         // second box takes the 3/4 of the width
539         val secondGapSize = ((rootSize * 3 / 4) - 10.dp) / 2
540         rule.waitForIdle()
541         expectedLeft += firstGapSize
542 
543         rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(expectedLeft, expectedTop)
544         expectedLeft += 10.dp + firstGapSize + secondGapSize
545         rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(expectedLeft, expectedTop)
546     }
547 
548     @Test
549     fun testGaps() {
550         val rootSize = 200.dp
551         val hGap = 10.dp
552         val vGap = 20.dp
553         rule.setContent {
554             gridComposableGapTest(
555                 modifier = Modifier.size(rootSize),
556                 width = "'parent'",
557                 height = "'parent'",
558                 hGap = Math.round(hGap.value),
559                 vGap = Math.round(vGap.value),
560             )
561         }
562         var expectedLeft = 0.dp
563         var expectedTop = 0.dp
564 
565         val boxWidth = (rootSize - hGap) / 2f
566         val boxHeight = (rootSize - vGap) / 2f
567 
568         rule.waitForIdle()
569         rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(0.dp, 0.dp)
570         expectedLeft += boxWidth + hGap
571         rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(expectedLeft, 0.dp)
572         expectedTop += boxHeight + vGap
573         rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(0.dp, expectedTop)
574         rule.onNodeWithTag("box3").assertPositionInRootIsEqualTo(expectedLeft, expectedTop)
575     }
576 
577     @Composable
578     private fun gridComposableTest(
579         modifier: Modifier = Modifier,
580         width: String,
581         height: String,
582         rows: Int,
583         columns: Int,
584         spans: String,
585         skips: String,
586         rowWeights: String,
587         columnWeights: String,
588         boxesCount: Int,
589         orientation: Int,
590         vGap: Int,
591         hGap: Int,
592         flags: String,
593     ) {
594         val ids = (0 until boxesCount).map { "box$it" }.toTypedArray()
595         val gridContains = ids.joinToString(separator = ", ") { "'$it'" }
596         ConstraintLayout(
597             modifier = modifier,
598             constraintSet =
599                 ConstraintSet(
600                     """
601         {
602             grid: {
603                 width: $width,
604                 height: $height,
605                 type: 'grid',
606                 rows: $rows,
607                 columns $columns,
608                 vGap: $vGap,
609                 hGap: $hGap,
610                 spans: $spans,
611                 skips: $skips,
612                 rowWeights: $rowWeights,
613                 columnWeights: $columnWeights
614                 orientation: $orientation,
615                 contains: [$gridContains],
616                 flags: $flags,
617               }
618         }
619         """
620                         .trimIndent()
621                 )
622         ) {
623             ids.forEach { id ->
624                 Box(Modifier.layoutId(id).size(10.dp).background(Color.Red).testTag(id))
625             }
626         }
627     }
628 
629     @Composable
630     private fun gridComposableGapTest(
631         modifier: Modifier = Modifier,
632         width: String,
633         height: String,
634         vGap: Int,
635         hGap: Int,
636     ) {
637         val ids = (0 until 4).map { "box$it" }.toTypedArray()
638         val gridContains = ids.joinToString(separator = ", ") { "'$it'" }
639 
640         ConstraintLayout(
641             modifier = modifier,
642             constraintSet =
643                 ConstraintSet(
644                     """
645         {
646             grid: {
647                 width: $width,
648                 height: $height,
649                 type: 'grid',
650                 rows: 2,
651                 columns: 2,
652                 vGap: $vGap,
653                 hGap: $hGap,
654                 contains: [$gridContains],
655               },
656               box0: {
657                 width: 'spread',
658                 height: 'spread',
659               },
660               box1: {
661                 width: 'spread',
662                 height: 'spread',
663               },
664               box2: {
665                 width: 'spread',
666                 height: 'spread',
667               },
668               box3: {
669                 width: 'spread',
670                 height: 'spread',
671               }
672         }
673         """
674                         .trimIndent()
675                 )
676         ) {
677             ids.forEach { id -> Box(Modifier.layoutId(id).background(Color.Red).testTag(id)) }
678         }
679     }
680 }
681