Skip to content

Commit 7a69f83

Browse files
Andrew Wangfacebook-github-bot
authored andcommitted
Add unit tests for calculateLayout
Summary: Adding more tests and refactoring few code. Reviewed By: adityasharat Differential Revision: D77943980 fbshipit-source-id: 5d110c9c023d18e968a678eaf7e20ee1daf0cd3d
1 parent e3d229b commit 7a69f83

File tree

2 files changed

+427
-76
lines changed

2 files changed

+427
-76
lines changed

litho-widget-kotlin/src/test/kotlin/com/facebook/litho/widget/collection/CollectionLayoutFunctionsTest.kt

Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616

1717
package com.facebook.litho.widget.collection
1818

19+
import androidx.recyclerview.widget.LinearLayoutManager
1920
import com.facebook.litho.testing.LithoTestRule
2021
import com.facebook.litho.testing.exactly
2122
import com.facebook.litho.testing.testrunner.LithoTestRunner
2223
import com.facebook.litho.widget.CollectionLayoutScope
2324
import com.facebook.litho.widget.ComponentRenderInfo
2425
import com.facebook.litho.widget.LinearLayoutInfo
2526
import com.facebook.litho.widget.LithoCollectionItem
27+
import com.facebook.litho.widget.calculateLayout
2628
import com.facebook.litho.widget.getChildSizeConstraints
2729
import com.facebook.rendercore.Size
2830
import com.facebook.rendercore.SizeConstraints
@@ -356,4 +358,369 @@ class CollectionLayoutFunctionsTest {
356358
}
357359

358360
// endregion
361+
362+
// region CollectionLayoutFunctions::calculateLayout
363+
364+
// Tests calculateLayout when collection has exact size constraints
365+
// Condition:
366+
// - Collection has exact size constraints (300x300) - both width and height are fixed
367+
// - Collection size is not pre-calculated (null)
368+
// - Empty list of items to layout
369+
// Expectation:
370+
// - Should return exact size matching the constraints (300x300)
371+
// - Width and height should both be 300 regardless of item content
372+
@Test
373+
fun `calculateLayout with exact collection constraints`() {
374+
val linearLayoutInfo = mock<LinearLayoutInfo>()
375+
val scope =
376+
CollectionLayoutScope(
377+
layoutInfo = linearLayoutInfo,
378+
collectionConstraints = SizeConstraints.exact(300, 300),
379+
collectionSize = null,
380+
isVertical = true,
381+
wrapInMainAxis = false,
382+
crossAxisWrapMode = CrossAxisWrapMode.NoWrap,
383+
)
384+
385+
val result = scope.calculateLayout(emptyList())
386+
387+
assertThat(result).isNotNull
388+
assertThat(result.width).isEqualTo(300)
389+
assertThat(result.height).isEqualTo(300)
390+
}
391+
392+
// Tests calculateLayout with maximum collection constraints and no wrapping
393+
// Condition:
394+
// - Collection has maximum size constraints (maxWidth = 400, maxHeight = 500)
395+
// - Collection size is not pre-calculated (null)
396+
// - wrapInMainAxis = false: collection uses full available space in main axis
397+
// - CrossAxisWrapMode.NoWrap: collection uses full available space in cross axis
398+
// - Empty list of items to layout
399+
// Expectation:
400+
// - Should return size matching the maximum constraints (400x500)
401+
// - Width and height should use full available space regardless of item content
402+
@Test
403+
fun `calculateLayout with maximum collection constraints and no wrapping`() {
404+
val linearLayoutInfo = mock<LinearLayoutInfo>()
405+
val scope =
406+
CollectionLayoutScope(
407+
layoutInfo = linearLayoutInfo,
408+
collectionConstraints = SizeConstraints(maxWidth = 400, maxHeight = 500),
409+
collectionSize = null,
410+
isVertical = true,
411+
wrapInMainAxis = false,
412+
crossAxisWrapMode = CrossAxisWrapMode.NoWrap,
413+
)
414+
415+
val result = scope.calculateLayout(emptyList())
416+
417+
assertThat(result).isNotNull
418+
assertThat(result.width).isEqualTo(400)
419+
assertThat(result.height).isEqualTo(500)
420+
}
421+
422+
// Tests calculateLayout when cross-axis wrapping matches the first child's size
423+
// Condition:
424+
// - CrossAxisWrapMode is MatchFirstChild - collection width should match first item
425+
// - Vertical orientation - items are stacked vertically, width varies per item
426+
// - Collection has bounded height constraint (maxHeight = 400) but flexible width
427+
// - Two items with different widths: first item 100px wide, second item 200px wide
428+
// Expectation:
429+
// - Collection width should match the first item's width (100px), ignoring other items
430+
// - Collection height should use the maximum available height (400px)
431+
// - Final size should be 100x400, not influenced by the second item's 200px width
432+
@Test
433+
fun `calculateLayout with MatchFirstChild wrap mode vertical`() {
434+
val linearLayoutInfo = mock<LinearLayoutInfo>()
435+
whenever(linearLayoutInfo.createViewportFiller(any(), any())).thenReturn(null)
436+
437+
val scope =
438+
spy(
439+
CollectionLayoutScope(
440+
layoutInfo = linearLayoutInfo,
441+
collectionConstraints = SizeConstraints(maxHeight = 400),
442+
collectionSize = null,
443+
isVertical = true,
444+
wrapInMainAxis = false,
445+
crossAxisWrapMode = CrossAxisWrapMode.MatchFirstChild,
446+
))
447+
448+
// Create items with different sizes
449+
val renderInfo1 = ComponentRenderInfo.createEmpty()
450+
val firstItem = LithoCollectionItem(lithoTestRule.context, renderInfo = renderInfo1)
451+
whenever(linearLayoutInfo.getChildWidthSpec(any(), eq(renderInfo1))).thenReturn(exactly(100))
452+
val renderInfo2 = ComponentRenderInfo.createEmpty()
453+
val secondItem = LithoCollectionItem(lithoTestRule.context, renderInfo = renderInfo2)
454+
whenever(linearLayoutInfo.getChildWidthSpec(any(), eq(renderInfo2))).thenReturn(exactly(200))
455+
456+
val items = listOf(firstItem, secondItem)
457+
458+
val result = scope.calculateLayout(items)
459+
460+
assertThat(result).isNotNull
461+
assertThat(result.width).isEqualTo(100)
462+
assertThat(result.height).isEqualTo(400)
463+
}
464+
465+
// Tests calculateLayout with MatchFirstChild wrap mode in horizontal orientation
466+
// Condition:
467+
// - Collection is horizontally oriented (items arranged left-to-right)
468+
// - CrossAxisWrapMode.MatchFirstChild: collection's cross-axis size matches first item
469+
// - Collection has flexible width constraint (maxWidth = 400) but no fixed size
470+
// - Two items with different cross-axis (height) sizes: first=200px, second=100px
471+
// Expectation:
472+
// - Collection width: uses maximum available width (400px) since width is main axis
473+
// - Collection height: matches first item's height (200px), ignoring second item's 100px
474+
// - Final size: 400x200 (width from constraint, height from first item)
475+
@Test
476+
fun `calculateLayout with MatchFirstChild wrap mode horizontal`() {
477+
val linearLayoutInfo = mock<LinearLayoutInfo>()
478+
whenever(linearLayoutInfo.createViewportFiller(any(), any())).thenReturn(null)
479+
val scope =
480+
CollectionLayoutScope(
481+
layoutInfo = linearLayoutInfo,
482+
collectionConstraints = SizeConstraints(maxWidth = 400),
483+
collectionSize = null,
484+
isVertical = false,
485+
wrapInMainAxis = false,
486+
crossAxisWrapMode = CrossAxisWrapMode.MatchFirstChild,
487+
)
488+
489+
// Create items with different sizes
490+
val renderInfo1 = ComponentRenderInfo.createEmpty()
491+
val firstItem = LithoCollectionItem(lithoTestRule.context, renderInfo = renderInfo1)
492+
whenever(linearLayoutInfo.getChildHeightSpec(any(), eq(renderInfo1))).thenReturn(exactly(200))
493+
val renderInfo2 = ComponentRenderInfo.createEmpty()
494+
val secondItem = LithoCollectionItem(lithoTestRule.context, renderInfo = renderInfo2)
495+
whenever(linearLayoutInfo.getChildHeightSpec(any(), eq(renderInfo2))).thenReturn(exactly(100))
496+
497+
val items = listOf(firstItem, secondItem)
498+
499+
val result = scope.calculateLayout(items)
500+
501+
assertThat(result).isNotNull
502+
assertThat(result.width).isEqualTo(400)
503+
assertThat(result.height).isEqualTo(200)
504+
}
505+
506+
// Tests calculateLayout with Dynamic wrap mode in vertical orientation
507+
// Condition:
508+
// - Collection is vertically oriented (items stacked top-to-bottom)
509+
// - CrossAxisWrapMode.Dynamic: collection's cross-axis size adapts to largest item
510+
// - Collection has flexible height constraint (maxHeight = 400) but no fixed size
511+
// - Five items with varying cross-axis (width) sizes: 100px, 200px, 150px, 230px, 120px
512+
// Expectation:
513+
// - Collection width: matches largest item width (230px from fourth item)
514+
// - Collection height: uses maximum available height (400px) since height is main axis
515+
// - Final size: 230x400 (width from largest item, height from constraint)
516+
@Test
517+
fun `calculateLayout with Dynamic wrap mode vertical`() {
518+
val linearLayoutInfo =
519+
spy(LinearLayoutInfo(LinearLayoutManager(lithoTestRule.context.androidContext)))
520+
521+
val scope =
522+
spy(
523+
CollectionLayoutScope(
524+
layoutInfo = linearLayoutInfo,
525+
collectionConstraints = SizeConstraints(maxHeight = 400),
526+
collectionSize = null,
527+
isVertical = true,
528+
wrapInMainAxis = false,
529+
crossAxisWrapMode = CrossAxisWrapMode.Dynamic,
530+
))
531+
532+
// Create items with different sizes
533+
val renderInfo1 = ComponentRenderInfo.createEmpty()
534+
val firstItem = spy(LithoCollectionItem(lithoTestRule.context, renderInfo = renderInfo1))
535+
whenever(linearLayoutInfo.getChildHeightSpec(any(), eq(renderInfo1))).thenReturn(exactly(50))
536+
whenever(firstItem.size).thenReturn(Size(100, 50))
537+
val renderInfo2 = ComponentRenderInfo.createEmpty()
538+
val secondItem = spy(LithoCollectionItem(lithoTestRule.context, renderInfo = renderInfo2))
539+
whenever(linearLayoutInfo.getChildHeightSpec(any(), eq(renderInfo2))).thenReturn(exactly(75))
540+
whenever(secondItem.size).thenReturn(Size(200, 75))
541+
val renderInfo3 = ComponentRenderInfo.createEmpty()
542+
val thirdItem = spy(LithoCollectionItem(lithoTestRule.context, renderInfo = renderInfo3))
543+
whenever(linearLayoutInfo.getChildHeightSpec(any(), eq(renderInfo3))).thenReturn(exactly(60))
544+
whenever(thirdItem.size).thenReturn(Size(150, 60))
545+
val renderInfo4 = ComponentRenderInfo.createEmpty()
546+
val fourthItem = spy(LithoCollectionItem(lithoTestRule.context, renderInfo = renderInfo4))
547+
whenever(linearLayoutInfo.getChildHeightSpec(any(), eq(renderInfo4))).thenReturn(exactly(90))
548+
whenever(fourthItem.size).thenReturn(Size(230, 90))
549+
val renderInfo5 = ComponentRenderInfo.createEmpty()
550+
val fifthItem = spy(LithoCollectionItem(lithoTestRule.context, renderInfo = renderInfo5))
551+
whenever(linearLayoutInfo.getChildHeightSpec(any(), eq(renderInfo5))).thenReturn(exactly(40))
552+
whenever(fifthItem.size).thenReturn(Size(120, 40))
553+
554+
val items = listOf(firstItem, secondItem, thirdItem, fourthItem, fifthItem)
555+
556+
val result = scope.calculateLayout(items)
557+
558+
assertThat(result).isNotNull
559+
assertThat(result.width).isEqualTo(230) // Largest width
560+
assertThat(result.height).isEqualTo(400)
561+
}
562+
563+
// Tests calculateLayout with Dynamic wrap mode in horizontal orientation
564+
// Condition:
565+
// - Collection is horizontally oriented (items arranged left-to-right)
566+
// - CrossAxisWrapMode.Dynamic: collection's cross-axis size adapts to largest item
567+
// - Collection has flexible width constraint (maxWidth = 400) but no fixed size
568+
// - Five items with varying cross-axis (height) sizes: 100px, 200px, 150px, 230px, 120px
569+
// Expectation:
570+
// - Collection width: uses maximum available width (400px) since width is main axis
571+
// - Collection height: matches largest item height (230px from fourth item)
572+
// - Final size: 400x230 (width from constraint, height from largest item)
573+
@Test
574+
fun `calculateLayout with Dynamic wrap mode horizontal`() {
575+
val linearLayoutInfo =
576+
spy(
577+
LinearLayoutInfo(
578+
LinearLayoutManager(
579+
lithoTestRule.context.androidContext, LinearLayoutManager.HORIZONTAL, false)))
580+
581+
val scope =
582+
spy(
583+
CollectionLayoutScope(
584+
layoutInfo = linearLayoutInfo,
585+
collectionConstraints = SizeConstraints(maxWidth = 400),
586+
collectionSize = null,
587+
isVertical = false,
588+
wrapInMainAxis = false,
589+
crossAxisWrapMode = CrossAxisWrapMode.Dynamic,
590+
))
591+
592+
// Create items with different sizes
593+
val renderInfo1 = ComponentRenderInfo.createEmpty()
594+
val firstItem = spy(LithoCollectionItem(lithoTestRule.context, renderInfo = renderInfo1))
595+
whenever(linearLayoutInfo.getChildWidthSpec(any(), eq(renderInfo1))).thenReturn(exactly(50))
596+
whenever(firstItem.size).thenReturn(Size(50, 100))
597+
val renderInfo2 = ComponentRenderInfo.createEmpty()
598+
val secondItem = spy(LithoCollectionItem(lithoTestRule.context, renderInfo = renderInfo2))
599+
whenever(linearLayoutInfo.getChildWidthSpec(any(), eq(renderInfo2))).thenReturn(exactly(75))
600+
whenever(secondItem.size).thenReturn(Size(75, 200))
601+
val renderInfo3 = ComponentRenderInfo.createEmpty()
602+
val thirdItem = spy(LithoCollectionItem(lithoTestRule.context, renderInfo = renderInfo3))
603+
whenever(linearLayoutInfo.getChildWidthSpec(any(), eq(renderInfo3))).thenReturn(exactly(60))
604+
whenever(thirdItem.size).thenReturn(Size(60, 150))
605+
val renderInfo4 = ComponentRenderInfo.createEmpty()
606+
val fourthItem = spy(LithoCollectionItem(lithoTestRule.context, renderInfo = renderInfo4))
607+
whenever(linearLayoutInfo.getChildWidthSpec(any(), eq(renderInfo4))).thenReturn(exactly(90))
608+
whenever(fourthItem.size).thenReturn(Size(90, 230))
609+
val renderInfo5 = ComponentRenderInfo.createEmpty()
610+
val fifthItem = spy(LithoCollectionItem(lithoTestRule.context, renderInfo = renderInfo5))
611+
whenever(linearLayoutInfo.getChildWidthSpec(any(), eq(renderInfo5))).thenReturn(exactly(40))
612+
whenever(fifthItem.size).thenReturn(Size(40, 120))
613+
614+
val items = listOf(firstItem, secondItem, thirdItem, fourthItem, fifthItem)
615+
616+
val result = scope.calculateLayout(items)
617+
618+
assertThat(result).isNotNull
619+
assertThat(result.width).isEqualTo(400)
620+
assertThat(result.height).isEqualTo(230) // Largest height
621+
}
622+
623+
// Tests calculateLayout with main-axis wrapping enabled in vertical orientation
624+
// Condition:
625+
// - Collection is vertically oriented (items stacked top-to-bottom)
626+
// - CrossAxisWrapMode.NoWrap: collection width is constrained, not adaptive
627+
// - wrapInMainAxis = true: collection height wraps to fit content instead of using max
628+
// constraint
629+
// - Collection has size constraints (maxWidth = 300, maxHeight = 400)
630+
// - Two items with heights 50px and 75px, both using full width (300px)
631+
// Expectation:
632+
// - Collection width: uses maximum available width (300px) since cross-axis doesn't wrap
633+
// - Collection height: wraps to sum of item heights (50 + 75 = 125px), ignoring maxHeight
634+
// constraint
635+
// - Final size: 300x125 (width from constraint, height from content wrapping)
636+
@Test
637+
fun `calculateLayout with wrapInMainAxis and vertical`() {
638+
val linearLayoutInfo =
639+
spy(LinearLayoutInfo(LinearLayoutManager(lithoTestRule.context.androidContext)))
640+
641+
val scope =
642+
spy(
643+
CollectionLayoutScope(
644+
layoutInfo = linearLayoutInfo,
645+
collectionConstraints = SizeConstraints(maxWidth = 300, maxHeight = 400),
646+
collectionSize = null,
647+
isVertical = true,
648+
wrapInMainAxis = true,
649+
crossAxisWrapMode = CrossAxisWrapMode.NoWrap,
650+
))
651+
652+
// Create items with different sizes
653+
val renderInfo1 = ComponentRenderInfo.createEmpty()
654+
val firstItem = spy(LithoCollectionItem(lithoTestRule.context, renderInfo = renderInfo1))
655+
whenever(linearLayoutInfo.getChildWidthSpec(any(), eq(renderInfo1))).thenReturn(exactly(300))
656+
whenever(linearLayoutInfo.getChildHeightSpec(any(), eq(renderInfo1))).thenReturn(exactly(50))
657+
whenever(firstItem.size).thenReturn(Size(300, 50))
658+
val renderInfo2 = ComponentRenderInfo.createEmpty()
659+
val secondItem = spy(LithoCollectionItem(lithoTestRule.context, renderInfo = renderInfo2))
660+
whenever(linearLayoutInfo.getChildWidthSpec(any(), eq(renderInfo2))).thenReturn(exactly(300))
661+
whenever(linearLayoutInfo.getChildHeightSpec(any(), eq(renderInfo2))).thenReturn(exactly(75))
662+
whenever(secondItem.size).thenReturn(Size(300, 75))
663+
664+
val items = listOf(firstItem, secondItem)
665+
666+
val result = scope.calculateLayout(items)
667+
668+
assertThat(result).isNotNull
669+
assertThat(result.width).isEqualTo(300)
670+
assertThat(result.height).isEqualTo(125) // Sum of item heights: 50 + 75
671+
}
672+
673+
// Tests calculateLayout with main-axis wrapping enabled in horizontal orientation
674+
// Condition:
675+
// - Collection is horizontally oriented (items arranged left-to-right)
676+
// - CrossAxisWrapMode.NoWrap: collection height is constrained, not adaptive
677+
// - wrapInMainAxis = true: collection width wraps to fit content instead of using max constraint
678+
// - Collection has size constraints (maxWidth = 400, maxHeight = 300)
679+
// - Two items with widths 50px and 75px, both using full height (300px)
680+
// Expectation:
681+
// - Collection width: wraps to sum of item widths (50 + 75 = 125px), ignoring maxWidth
682+
// constraint
683+
// - Collection height: uses maximum available height (300px) since cross-axis doesn't wrap
684+
// - Final size: 125x300 (width from content wrapping, height from constraint)
685+
@Test
686+
fun `calculateLayout with wrapInMainAxis and horizontal`() {
687+
val linearLayoutInfo =
688+
spy(
689+
LinearLayoutInfo(
690+
LinearLayoutManager(
691+
lithoTestRule.context.androidContext, LinearLayoutManager.HORIZONTAL, false)))
692+
693+
val scope =
694+
spy(
695+
CollectionLayoutScope(
696+
layoutInfo = linearLayoutInfo,
697+
collectionConstraints = SizeConstraints(maxWidth = 400, maxHeight = 300),
698+
collectionSize = null,
699+
isVertical = false,
700+
wrapInMainAxis = true,
701+
crossAxisWrapMode = CrossAxisWrapMode.NoWrap,
702+
))
703+
704+
// Create items with different sizes
705+
val renderInfo1 = ComponentRenderInfo.createEmpty()
706+
val firstItem = spy(LithoCollectionItem(lithoTestRule.context, renderInfo = renderInfo1))
707+
whenever(linearLayoutInfo.getChildWidthSpec(any(), eq(renderInfo1))).thenReturn(exactly(50))
708+
whenever(linearLayoutInfo.getChildHeightSpec(any(), eq(renderInfo1))).thenReturn(exactly(300))
709+
whenever(firstItem.size).thenReturn(Size(50, 300))
710+
val renderInfo2 = ComponentRenderInfo.createEmpty()
711+
val secondItem = spy(LithoCollectionItem(lithoTestRule.context, renderInfo = renderInfo2))
712+
whenever(linearLayoutInfo.getChildWidthSpec(any(), eq(renderInfo2))).thenReturn(exactly(75))
713+
whenever(linearLayoutInfo.getChildHeightSpec(any(), eq(renderInfo2))).thenReturn(exactly(300))
714+
whenever(secondItem.size).thenReturn(Size(75, 300))
715+
716+
val items = listOf(firstItem, secondItem)
717+
718+
val result = scope.calculateLayout(items)
719+
720+
assertThat(result).isNotNull
721+
assertThat(result.width).isEqualTo(125) // Sum of item widths: 50 + 75
722+
assertThat(result.height).isEqualTo(300)
723+
}
724+
725+
// endregion
359726
}

0 commit comments

Comments
 (0)