diff --git a/redwood-layout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeSnapshotter.kt b/redwood-layout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeSnapshotter.kt new file mode 100644 index 0000000000..7ade40f398 --- /dev/null +++ b/redwood-layout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeSnapshotter.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package app.cash.redwood.layout.composeui + +import androidx.compose.runtime.Composable +import app.cash.paparazzi.Paparazzi +import app.cash.redwood.layout.Snapshotter + +class ComposeSnapshotter( + private val paparazzi: Paparazzi, + private val widget: @Composable () -> Unit, +) : Snapshotter { + override fun snapshot(name: String?) { + paparazzi.snapshot(composable = widget, name = name) + } +} diff --git a/redwood-layout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiBoxTest.kt b/redwood-layout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiBoxTest.kt index 747ddef805..aa2e0f785b 100644 --- a/redwood-layout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiBoxTest.kt +++ b/redwood-layout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiBoxTest.kt @@ -46,7 +46,5 @@ class ComposeUiBoxTest( override fun text(): Text<@Composable () -> Unit> = ComposeUiText() - override fun verifySnapshot(widget: @Composable () -> Unit, name: String?) { - paparazzi.snapshot(composable = widget, name = name) - } + override fun snapshotter(widget: @Composable () -> Unit) = ComposeSnapshotter(paparazzi, widget) } diff --git a/redwood-layout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiFlexContainerTest.kt b/redwood-layout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiFlexContainerTest.kt index 7ee0db3278..c3f94695ea 100644 --- a/redwood-layout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiFlexContainerTest.kt +++ b/redwood-layout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiFlexContainerTest.kt @@ -60,11 +60,7 @@ class ComposeUiFlexContainerTest( override fun text() = ComposeUiText() - override fun verifySnapshot(widget: @Composable () -> Unit, name: String?) { - paparazzi.snapshot(name) { - widget() - } - } + override fun snapshotter(widget: @Composable () -> Unit) = ComposeSnapshotter(paparazzi, widget) class ComposeTestFlexContainer private constructor( private val delegate: ComposeUiFlexContainer, diff --git a/redwood-layout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiSpacerTest.kt b/redwood-layout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiSpacerTest.kt index 4e820dfaf2..80388852d5 100644 --- a/redwood-layout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiSpacerTest.kt +++ b/redwood-layout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiSpacerTest.kt @@ -57,7 +57,5 @@ class ComposeUiSpacerTest : AbstractSpacerTest<@Composable () -> Unit>() { } } - override fun verifySnapshot(value: @Composable () -> Unit) { - paparazzi.snapshot(composable = value) - } + override fun snapshotter(widget: @Composable () -> Unit) = ComposeSnapshotter(paparazzi, widget) } diff --git a/redwood-layout-shared-test/src/commonMain/kotlin/app/cash/redwood/layout/AbstractBoxTest.kt b/redwood-layout-shared-test/src/commonMain/kotlin/app/cash/redwood/layout/AbstractBoxTest.kt index 86f9feefbe..7ed01ba526 100644 --- a/redwood-layout-shared-test/src/commonMain/kotlin/app/cash/redwood/layout/AbstractBoxTest.kt +++ b/redwood-layout-shared-test/src/commonMain/kotlin/app/cash/redwood/layout/AbstractBoxTest.kt @@ -38,12 +38,12 @@ abstract class AbstractBoxTest { height(height) } - abstract fun verifySnapshot(widget: T, name: String? = null) + abstract fun snapshotter(widget: T): Snapshotter @Test fun testEmpty_Defaults() { val widget = box() - verifySnapshot(widget.value) + snapshotter(widget.value).snapshot() } @Test @@ -52,7 +52,7 @@ abstract class AbstractBoxTest { width(Constraint.Wrap) height(Constraint.Wrap) } - verifySnapshot(widget.value) + snapshotter(widget.value).snapshot() } @Test @@ -61,7 +61,7 @@ abstract class AbstractBoxTest { width(Constraint.Fill) height(Constraint.Fill) } - verifySnapshot(widget.value) + snapshotter(widget.value).snapshot() } // testChildren @@ -322,7 +322,7 @@ abstract class AbstractBoxTest { }, ) } - verifySnapshot(widget.value) + snapshotter(widget.value).snapshot() } @Test @@ -345,7 +345,7 @@ abstract class AbstractBoxTest { }, ) } - verifySnapshot(widget.value) + snapshotter(widget.value).snapshot() } @Test @@ -388,7 +388,7 @@ abstract class AbstractBoxTest { }, ) } - verifySnapshot(widget.value) + snapshotter(widget.value).snapshot() } @Test @@ -431,7 +431,7 @@ abstract class AbstractBoxTest { }, ) } - verifySnapshot(widget.value) + snapshotter(widget.value).snapshot() } @Test @@ -444,10 +444,11 @@ abstract class AbstractBoxTest { children.insert(1, coloredText(text = mediumText(), color = Blue)) children.insert(2, coloredText(text = shortText(), color = Green)) } - verifySnapshot(widget.value, "Margin") + val snapshotter = snapshotter(widget.value) + snapshotter.snapshot("Margin") redColor.modifier = Modifier widget.children.onModifierUpdated(0, redColor) - verifySnapshot(widget.value, "Empty") + snapshotter.snapshot("Empty") } /** The view shouldn't crash if its displayed after being detached. */ @@ -459,16 +460,17 @@ abstract class AbstractBoxTest { horizontalAlignment(CrossAxisAlignment.Start) verticalAlignment(CrossAxisAlignment.Start) } + val snapshotter = snapshotter(widget.value) // Render before calling detach(). widget.children.insert(0, coloredText(MarginImpl(10.dp), mediumText(), Green)) widget.children.insert(1, coloredText(MarginImpl(0.dp), shortText(), Blue)) - verifySnapshot(widget.value, "Before") + snapshotter.snapshot("Before") // Detach after changes are applied but before they're rendered. widget.children.insert(0, coloredText(MarginImpl(20.dp), longText(), Red)) widget.children.detach() - verifySnapshot(widget.value, "After") + snapshotter.snapshot("After") } @Test fun testDynamicWidgetResizing() { @@ -479,6 +481,7 @@ abstract class AbstractBoxTest { horizontalAlignment(CrossAxisAlignment.Start) verticalAlignment(CrossAxisAlignment.Start) } + val snapshotter = snapshotter(container.value) val a = coloredText(text = "AAA", color = Red) .apply { modifier = HorizontalAlignmentImpl(CrossAxisAlignment.Start) } @@ -489,10 +492,10 @@ abstract class AbstractBoxTest { val c = coloredText(text = "CCC", color = Green) .apply { modifier = HorizontalAlignmentImpl(CrossAxisAlignment.End) } .also { container.children.insert(2, it) } - verifySnapshot(container.value, "v1") + snapshotter.snapshot("v1") b.text("BBB_v2") - verifySnapshot(container.value, "v2") + snapshotter.snapshot("v2") } private fun coloredText(modifier: Modifier = Modifier, text: String, color: Int) = text().apply { diff --git a/redwood-layout-shared-test/src/commonMain/kotlin/app/cash/redwood/layout/AbstractFlexContainerTest.kt b/redwood-layout-shared-test/src/commonMain/kotlin/app/cash/redwood/layout/AbstractFlexContainerTest.kt index e57648e225..9257e1a1ae 100644 --- a/redwood-layout-shared-test/src/commonMain/kotlin/app/cash/redwood/layout/AbstractFlexContainerTest.kt +++ b/redwood-layout-shared-test/src/commonMain/kotlin/app/cash/redwood/layout/AbstractFlexContainerTest.kt @@ -65,10 +65,7 @@ abstract class AbstractFlexContainerTest { return widget } - abstract fun verifySnapshot( - widget: T, - name: String? = null, - ) + abstract fun snapshotter(widget: T): Snapshotter @Test fun testEmptyLayout_Column() { emptyLayout(FlexDirection.Column) @@ -85,7 +82,7 @@ abstract class AbstractFlexContainerTest { val container = flexContainer(flexDirection) container.crossAxisAlignment(CrossAxisAlignment.Start) container.onEndChanges() - verifySnapshot(container.value) + snapshotter(container.value).snapshot() } @Test fun testLayoutWithConstraints_Column_Wrap_Wrap() { @@ -131,7 +128,7 @@ abstract class AbstractFlexContainerTest { container.height(height) container.add(text(movies.first())) container.onEndChanges() - verifySnapshot(container.value) + snapshotter(container.value).snapshot() } @Test fun testShortLayout_Column() { @@ -152,7 +149,7 @@ abstract class AbstractFlexContainerTest { container.add(text(movie)) } container.onEndChanges() - verifySnapshot(container.value) + snapshotter(container.value).snapshot() } @Test fun testLongLayout_Column() { @@ -173,7 +170,7 @@ abstract class AbstractFlexContainerTest { container.add(text(movie)) } container.onEndChanges() - verifySnapshot(container.value) + snapshotter(container.value).snapshot() } @Test fun testLayoutWithMarginAndDifferentAlignments_Column() { @@ -202,7 +199,7 @@ abstract class AbstractFlexContainerTest { container.add(text(movie, modifier)) } container.onEndChanges() - verifySnapshot(container.value) + snapshotter(container.value).snapshot() } @Test fun testLayoutWithCrossAxisAlignment_Column_Start() { @@ -250,11 +247,12 @@ abstract class AbstractFlexContainerTest { container.add(text(movie)) } container.onEndChanges() - verifySnapshot(container.value) + snapshotter(container.value).snapshot() } @Test fun columnWithUpdatedCrossAxisAlignment() { val container = flexContainer(FlexDirection.Column) + val snapshotter = snapshotter(container.value) container.width(Constraint.Fill) container.height(Constraint.Fill) container.crossAxisAlignment(CrossAxisAlignment.Center) @@ -262,10 +260,10 @@ abstract class AbstractFlexContainerTest { container.add(text(movie)) } container.onEndChanges() - verifySnapshot(container.value, "Center") + snapshotter.snapshot("Center") container.crossAxisAlignment(CrossAxisAlignment.End) container.onEndChanges() - verifySnapshot(container.value, "FlexEnd") + snapshotter.snapshot("FlexEnd") } @Test fun testColumnWithMainAxisAlignment_Center() { @@ -293,7 +291,7 @@ abstract class AbstractFlexContainerTest { container.add(text(movie)) } container.onEndChanges() - verifySnapshot(container.value) + snapshotter(container.value).snapshot() } @Test fun testContainerWithFixedWidthItems() { @@ -305,7 +303,7 @@ abstract class AbstractFlexContainerTest { container.add(text("$index", WidthImpl(50.dp))) } container.onEndChanges() - verifySnapshot(container.value) + snapshotter(container.value).snapshot() } @Test fun testContainerWithFixedHeightItems() { @@ -317,7 +315,7 @@ abstract class AbstractFlexContainerTest { container.add(text("$index", HeightImpl(50.dp))) } container.onEndChanges() - verifySnapshot(container.value) + snapshotter(container.value).snapshot() } @Test fun testContainerWithFixedSizeItems() { @@ -329,21 +327,22 @@ abstract class AbstractFlexContainerTest { container.add(text("$index", SizeImpl(50.dp, 50.dp))) } container.onEndChanges() - verifySnapshot(container.value) + snapshotter(container.value).snapshot() } @Test fun testChildWithUpdatedProperty() { val container = flexContainer(FlexDirection.Column) + val snapshotter = snapshotter(container.value) container.width(Constraint.Fill) container.height(Constraint.Fill) container.crossAxisAlignment(CrossAxisAlignment.Start) val widget = text("") container.add(widget) container.onEndChanges() - verifySnapshot(container.value, "initial") + snapshotter.snapshot("initial") widget.text(movies.first()) container.onEndChanges() - verifySnapshot(container.value, "updated") + snapshotter.snapshot("updated") } @Test fun testColumnThenRow() { @@ -382,7 +381,7 @@ abstract class AbstractFlexContainerTest { }, ) - verifySnapshot(column.value) + snapshotter(column.value).snapshot() } /** This test demonstrates that margins are lost unless `shrink(1.0)` is added. */ @@ -436,11 +435,12 @@ abstract class AbstractFlexContainerTest { }, ) - verifySnapshot(column.value) + snapshotter(column.value).snapshot() } @Test fun testDynamicElementUpdates() { val container = flexContainer(FlexDirection.Column) + val snapshotter = snapshotter(container.value) container.width(Constraint.Fill) container.height(Constraint.Fill) container.add(text("A")) @@ -449,15 +449,15 @@ abstract class AbstractFlexContainerTest { container.add(text("E")) container.onEndChanges() - verifySnapshot(container.value, "ABDE") + snapshotter.snapshot("ABDE") container.addAt(index = 2, widget = text("C")) container.onEndChanges() - verifySnapshot(container.value, "ABCDE") + snapshotter.snapshot("ABCDE") container.removeAt(index = 0) container.onEndChanges() - verifySnapshot(container.value, "BCDE") + snapshotter.snapshot("BCDE") } @Test fun testDynamicContainerSize() { @@ -465,6 +465,7 @@ abstract class AbstractFlexContainerTest { width(Constraint.Fill) height(Constraint.Fill) } + val snapshotter = snapshotter(parent.value) parent.children.insert( 0, @@ -509,10 +510,10 @@ abstract class AbstractFlexContainerTest { }, ) - verifySnapshot(parent.value, "both") + snapshotter.snapshot("both") parent.children.remove(index = 1, count = 1) - verifySnapshot(parent.value, "single") + snapshotter.snapshot("single") } @Test fun testFlexDistributesWeightEqually() { @@ -523,7 +524,7 @@ abstract class AbstractFlexContainerTest { container.add(text("SHORTER TEXT", FlexImpl(1.0))) container.add(text("A", FlexImpl(1.0))) container.add(text("LINE1\nLINE2\nLINE3", FlexImpl(1.0))) - verifySnapshot(container.value) + snapshotter(container.value).snapshot() } @Test fun testFlexDistributesWeightUnequally() { @@ -534,7 +535,7 @@ abstract class AbstractFlexContainerTest { container.add(text("SHORTER TEXT", FlexImpl(1.0))) container.add(text("A", FlexImpl(1.0))) container.add(text("LINE1\nLINE2\nLINE3", FlexImpl(1.0))) - verifySnapshot(container.value) + snapshotter(container.value).snapshot() } @Test fun testNestedColumnsWithFlex() { @@ -561,16 +562,14 @@ abstract class AbstractFlexContainerTest { outerContainer.add(innerContainer2) innerContainer2.modifier = Modifier.then(FlexImpl(1.0)) outerContainer.children.onModifierUpdated(1, innerContainer2) - verifySnapshot(outerContainer.value) + snapshotter(outerContainer.value).snapshot() } - @Test - fun testColumnWithChildModifierChanges() { + @Test fun testColumnWithChildModifierChanges() { testContainerWithChildrenModifierChanges(FlexDirection.Column) } - @Test - fun testRowWithChildModifierChanges() { + @Test fun testRowWithChildModifierChanges() { testContainerWithChildrenModifierChanges(FlexDirection.Row) } @@ -578,6 +577,7 @@ abstract class AbstractFlexContainerTest { flexDirection: FlexDirection, ) { val container = flexContainer(flexDirection) + val snapshotter = snapshotter(container.value) container.width(Constraint.Fill) container.height(Constraint.Fill) @@ -588,37 +588,35 @@ abstract class AbstractFlexContainerTest { container.add(text(mediumText(), backgroundColor = Green)) container.add(text(shortText(), backgroundColor = Blue)) container.onEndChanges() - verifySnapshot(container.value, "Margin") + snapshotter.snapshot("Margin") first.modifier = Modifier container.children.onModifierUpdated(0, first) container.onEndChanges() - verifySnapshot(container.value, "Empty") + snapshotter.snapshot("Empty") } /** The view shouldn't crash if its displayed after being detached. */ - @Test - fun testLayoutAfterDetach() { + @Test fun testLayoutAfterDetach() { val container = flexContainer(FlexDirection.Column).apply { width(Constraint.Fill) height(Constraint.Fill) } - val widget = container.value // Don't access widget.value after detach(). + val snapshotter = snapshotter(container.value) // Render before calling detach(). container.children.insert(0, text(mediumText(), MarginImpl(10.dp), Green)) container.children.insert(1, text(shortText(), MarginImpl(0.dp), Blue)) container.onEndChanges() - verifySnapshot(widget, "Before") + snapshotter.snapshot("Before") // Detach after changes are applied but before they're rendered. container.children.insert(0, text(longText(), MarginImpl(20.dp), Red)) container.onEndChanges() container.children.detach() - verifySnapshot(widget, "After") + snapshotter.snapshot("After") } - @Test - fun testOnScrollListener() { + @Test fun testOnScrollListener() { var scrolled = false val container = flexContainer(FlexDirection.Column).apply { width(Constraint.Fill) @@ -631,7 +629,7 @@ abstract class AbstractFlexContainerTest { container.scroll(Px(1000.0)) - verifySnapshot(container.value) + snapshotter(container.value).snapshot() assertTrue(scrolled) } @@ -644,6 +642,7 @@ abstract class AbstractFlexContainerTest { */ @Test fun testLayoutIsIncremental() { val container = flexContainer(FlexDirection.Column) + val snapshotter = snapshotter(container.value) container.width(Constraint.Fill) container.height(Constraint.Fill) container.crossAxisAlignment(CrossAxisAlignment.Start) @@ -658,13 +657,13 @@ abstract class AbstractFlexContainerTest { .apply { modifier = HeightImpl(100.dp) } .also { container.add(it) } container.onEndChanges() - verifySnapshot(container.value, "v1") + snapshotter.snapshot("v1") val aMeasureCountV1 = a.measureCount val bMeasureCountV1 = b.measureCount val cMeasureCountV1 = c.measureCount b.text("B v2") - verifySnapshot(container.value, "v2") + snapshotter.snapshot("v2") val aMeasureCountV2 = a.measureCount val bMeasureCountV2 = b.measureCount val cMeasureCountV2 = c.measureCount @@ -675,7 +674,7 @@ abstract class AbstractFlexContainerTest { assertEquals(cMeasureCountV1, cMeasureCountV2) } - verifySnapshot(container.value, "v3") + snapshotter.snapshot("v3") val aMeasureCountV3 = a.measureCount val bMeasureCountV3 = b.measureCount val cMeasureCountV3 = c.measureCount @@ -689,6 +688,7 @@ abstract class AbstractFlexContainerTest { @Test fun testRecursiveLayoutIsIncremental() { val container = flexContainer(FlexDirection.Column) + val snapshotter = snapshotter(container.value) container.width(Constraint.Fill) container.height(Constraint.Fill) container.crossAxisAlignment(CrossAxisAlignment.Start) @@ -721,13 +721,13 @@ abstract class AbstractFlexContainerTest { .apply { modifier = HeightImpl(100.dp) } .also { rowC.children.insert(0, it) } container.onEndChanges() - verifySnapshot(container.value, "v1") + snapshotter.snapshot("v1") val aMeasureCountV1 = a.measureCount val bMeasureCountV1 = b.measureCount val cMeasureCountV1 = c.measureCount b.text("B v2") - verifySnapshot(container.value, "v2") + snapshotter.snapshot("v2") val aMeasureCountV2 = a.measureCount val bMeasureCountV2 = b.measureCount val cMeasureCountV2 = c.measureCount @@ -738,7 +738,7 @@ abstract class AbstractFlexContainerTest { assertEquals(cMeasureCountV1, cMeasureCountV2) } - verifySnapshot(container.value, "v3") + snapshotter.snapshot("v3") val aMeasureCountV3 = a.measureCount val bMeasureCountV3 = b.measureCount val cMeasureCountV3 = c.measureCount diff --git a/redwood-layout-shared-test/src/commonMain/kotlin/app/cash/redwood/layout/AbstractSpacerTest.kt b/redwood-layout-shared-test/src/commonMain/kotlin/app/cash/redwood/layout/AbstractSpacerTest.kt index 549efcac21..63d72b2aae 100644 --- a/redwood-layout-shared-test/src/commonMain/kotlin/app/cash/redwood/layout/AbstractSpacerTest.kt +++ b/redwood-layout-shared-test/src/commonMain/kotlin/app/cash/redwood/layout/AbstractSpacerTest.kt @@ -26,7 +26,7 @@ abstract class AbstractSpacerTest { abstract fun wrap(widget: Widget, horizontal: Boolean): T - abstract fun verifySnapshot(value: T) + abstract fun snapshotter(widget: T): Snapshotter private fun widget(width: Int, height: Int): Spacer = widget().apply { width(width.dp) @@ -35,21 +35,21 @@ abstract class AbstractSpacerTest { @Test fun testZeroSpacer() { val widget = widget(width = 0, height = 0) - verifySnapshot(wrap(widget, horizontal = true)) + snapshotter(wrap(widget, horizontal = true)).snapshot() } @Test fun testWidthOnlySpacer() { val widget = widget(width = 100, height = 0) - verifySnapshot(wrap(widget, horizontal = true)) + snapshotter(wrap(widget, horizontal = true)).snapshot() } @Test fun testHeightOnlySpacer() { val widget = widget(width = 0, height = 100) - verifySnapshot(wrap(widget, horizontal = false)) + snapshotter(wrap(widget, horizontal = false)).snapshot() } @Test fun testBothSpacer() { val widget = widget(width = 100, height = 100) - verifySnapshot(wrap(widget, horizontal = false)) + snapshotter(wrap(widget, horizontal = false)).snapshot() } } diff --git a/redwood-layout-shared-test/src/commonMain/kotlin/app/cash/redwood/layout/Snapshotter.kt b/redwood-layout-shared-test/src/commonMain/kotlin/app/cash/redwood/layout/Snapshotter.kt new file mode 100644 index 0000000000..23ed8d9649 --- /dev/null +++ b/redwood-layout-shared-test/src/commonMain/kotlin/app/cash/redwood/layout/Snapshotter.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package app.cash.redwood.layout + +/** + * Captures snapshots of a subject view. + * + * The subject of the view hierarchy must do its own layout invalidation. This is particularly + * important for iOS UIView layouts, where the application layer is responsible for tracking layout + * changes. + */ +interface Snapshotter { + fun snapshot(name: String? = null) +} diff --git a/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewBoxTest.kt b/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewBoxTest.kt index 71e84d91cd..c667e2b44e 100644 --- a/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewBoxTest.kt +++ b/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewBoxTest.kt @@ -21,7 +21,6 @@ import app.cash.redwood.layout.Text import app.cash.redwood.layout.widget.Box import assertk.assertThat import kotlin.test.Test -import platform.CoreGraphics.CGRectMake import platform.CoreGraphics.CGSizeMake import platform.UIKit.UIColor import platform.UIKit.UIView @@ -44,21 +43,7 @@ class UIViewBoxTest( return UIViewText() } - override fun verifySnapshot(widget: UIView, name: String?) { - val screenSize = CGRectMake(0.0, 0.0, 390.0, 844.0) // iPhone 14. - widget.setFrame(screenSize) - - // Snapshot the container on a white background. - val frame = UIView().apply { - backgroundColor = UIColor.whiteColor - setFrame(screenSize) - addSubview(widget) - layoutIfNeeded() - } - - callback.verifySnapshot(frame, name) - widget.removeFromSuperview() - } + override fun snapshotter(widget: UIView) = UIViewSnapshotter.framed(callback, widget) @Test fun maxEachDimension() { diff --git a/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewFlexContainerTest.kt b/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewFlexContainerTest.kt index 41ce51d955..6b1a7ba8a4 100644 --- a/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewFlexContainerTest.kt +++ b/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewFlexContainerTest.kt @@ -33,7 +33,6 @@ import kotlin.test.assertEquals import kotlinx.cinterop.CValue import kotlinx.cinterop.cValue import kotlinx.cinterop.readValue -import platform.CoreGraphics.CGRectMake import platform.CoreGraphics.CGRectZero import platform.CoreGraphics.CGSize import platform.CoreGraphics.CGSizeMake @@ -62,10 +61,13 @@ class UIViewFlexContainerTest( class UIViewTestFlexContainer internal constructor( private val delegate: UIViewFlexContainer, ) : TestFlexContainer, + ResizableWidget, FlexContainer by delegate, ChangeListener by delegate { private var childCount = 0 + override var sizeListener: SizeListener? by delegate::sizeListener + override val children: Widget.Children = delegate.children init { @@ -95,25 +97,7 @@ class UIViewFlexContainerTest( } } - override fun verifySnapshot(widget: UIView, name: String?) { - val frame = layoutInFrame(widget) - - callback.verifySnapshot(frame, name) - widget.removeFromSuperview() - } - - private fun layoutInFrame(widget: UIView): UIView { - val screenSize = CGRectMake(0.0, 0.0, 390.0, 844.0) // iPhone 14. - widget.setFrame(screenSize) - - // Snapshot the container on a white background. - return UIView().apply { - backgroundColor = UIColor.whiteColor - setFrame(screenSize) - addSubview(widget) - layoutIfNeeded() - } - } + override fun snapshotter(widget: UIView) = UIViewSnapshotter.framed(callback, widget) /** * Confirm that calling [ResizableWidget.SizeListener] is sufficient to trigger a subsequent call @@ -145,12 +129,14 @@ class UIViewFlexContainerTest( add(widget) } - layoutInFrame(container.value) + val snapshotter = snapshotter(container.value) + + snapshotter.layoutSubject() assertEquals(1, layoutSubviewsCount) widget.sizeListener?.invalidateSize() - layoutInFrame(container.value) + snapshotter.layoutSubject() assertEquals(2, layoutSubviewsCount) } } diff --git a/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewSnapshotter.kt b/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewSnapshotter.kt new file mode 100644 index 0000000000..419f51f049 --- /dev/null +++ b/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewSnapshotter.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package app.cash.redwood.layout.uiview + +import app.cash.redwood.layout.Snapshotter +import platform.CoreGraphics.CGRectMake +import platform.UIKit.UIColor +import platform.UIKit.UIView + +/** Snapshot the subject on a white background. */ +class UIViewSnapshotter( + private val callback: UIViewSnapshotCallback, + private val subject: UIView, +) : Snapshotter { + + override fun snapshot(name: String?) { + layoutSubject() + callback.verifySnapshot(subject, name) + } + + /** Do layout without taking a snapshot. */ + fun layoutSubject() { + subject.layoutIfNeeded() + } + + companion object { + fun framed( + callback: UIViewSnapshotCallback, + widget: UIView, + ): UIViewSnapshotter { + val frame = UIView() + .apply { + val screenSize = CGRectMake(0.0, 0.0, 390.0, 844.0) // iPhone 14. + + backgroundColor = UIColor.whiteColor + setFrame(screenSize) + + widget.setFrame(screenSize) + addSubview(widget) + } + return UIViewSnapshotter(callback, frame) + } + } +} diff --git a/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewSpacerTest.kt b/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewSpacerTest.kt index 584e0e9ba9..08caf050d8 100644 --- a/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewSpacerTest.kt +++ b/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewSpacerTest.kt @@ -51,7 +51,5 @@ class UIViewSpacerTest( } } - override fun verifySnapshot(value: UIView) { - callback.verifySnapshot(value, null) - } + override fun snapshotter(widget: UIView) = UIViewSnapshotter(callback, widget) } diff --git a/redwood-layout-view/src/test/kotlin/app/cash/redwood/layout/view/ViewBoxTest.kt b/redwood-layout-view/src/test/kotlin/app/cash/redwood/layout/view/ViewBoxTest.kt index 0f426ec646..a8b6893817 100644 --- a/redwood-layout-view/src/test/kotlin/app/cash/redwood/layout/view/ViewBoxTest.kt +++ b/redwood-layout-view/src/test/kotlin/app/cash/redwood/layout/view/ViewBoxTest.kt @@ -22,6 +22,7 @@ import app.cash.paparazzi.DeviceConfig import app.cash.paparazzi.Paparazzi import app.cash.redwood.layout.AbstractBoxTest import app.cash.redwood.layout.Color +import app.cash.redwood.layout.Snapshotter import app.cash.redwood.layout.Text import app.cash.redwood.layout.widget.Box import com.android.resources.LayoutDirection @@ -56,18 +57,19 @@ class ViewBoxTest( return ViewText(paparazzi.context) } - override fun verifySnapshot(widget: View, name: String?) { + override fun snapshotter(widget: View): Snapshotter { + // Allow children to wrap. val container = object : FrameLayout(paparazzi.context) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - // Allow children to wrap. super.onMeasure( MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.AT_MOST), ) } + }.apply { + addView(widget) } - container.addView(widget) - paparazzi.snapshot(view = container, name = name) - container.removeView(widget) + + return ViewSnapshotter(paparazzi, container) } } diff --git a/redwood-layout-view/src/test/kotlin/app/cash/redwood/layout/view/ViewFlexContainerTest.kt b/redwood-layout-view/src/test/kotlin/app/cash/redwood/layout/view/ViewFlexContainerTest.kt index 8df2165d56..fa328be597 100644 --- a/redwood-layout-view/src/test/kotlin/app/cash/redwood/layout/view/ViewFlexContainerTest.kt +++ b/redwood-layout-view/src/test/kotlin/app/cash/redwood/layout/view/ViewFlexContainerTest.kt @@ -60,9 +60,7 @@ class ViewFlexContainerTest( override fun text() = ViewText(paparazzi.context) - override fun verifySnapshot(widget: View, name: String?) { - paparazzi.snapshot(widget, name) - } + override fun snapshotter(widget: View) = ViewSnapshotter(paparazzi, widget) class ViewTestFlexContainer internal constructor( private val delegate: ViewFlexContainer, diff --git a/redwood-layout-view/src/test/kotlin/app/cash/redwood/layout/view/ViewSnapshotter.kt b/redwood-layout-view/src/test/kotlin/app/cash/redwood/layout/view/ViewSnapshotter.kt new file mode 100644 index 0000000000..1abfe1c386 --- /dev/null +++ b/redwood-layout-view/src/test/kotlin/app/cash/redwood/layout/view/ViewSnapshotter.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package app.cash.redwood.layout.view + +import android.view.View +import app.cash.paparazzi.Paparazzi +import app.cash.redwood.layout.Snapshotter + +class ViewSnapshotter( + private val paparazzi: Paparazzi, + private val view: View, +) : Snapshotter { + override fun snapshot(name: String?) { + paparazzi.snapshot(view = view, name = name) + } +} diff --git a/redwood-layout-view/src/test/kotlin/app/cash/redwood/layout/view/ViewSpacerTest.kt b/redwood-layout-view/src/test/kotlin/app/cash/redwood/layout/view/ViewSpacerTest.kt index a33725ccbd..20b62cdac1 100644 --- a/redwood-layout-view/src/test/kotlin/app/cash/redwood/layout/view/ViewSpacerTest.kt +++ b/redwood-layout-view/src/test/kotlin/app/cash/redwood/layout/view/ViewSpacerTest.kt @@ -51,7 +51,5 @@ class ViewSpacerTest : AbstractSpacerTest() { } } - override fun verifySnapshot(value: View) { - paparazzi.snapshot(value) - } + override fun snapshotter(widget: View) = ViewSnapshotter(paparazzi, widget) } diff --git a/redwood-lazylayout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeSnapshotter.kt b/redwood-lazylayout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeSnapshotter.kt new file mode 100644 index 0000000000..7523621a9f --- /dev/null +++ b/redwood-lazylayout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeSnapshotter.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package app.cash.redwood.layout.composeui + +import androidx.compose.runtime.Composable +import app.cash.paparazzi.Paparazzi +import app.cash.redwood.layout.Snapshotter + +class ComposeSnapshotter( + private val paparazzi: Paparazzi, + private val widget: @Composable () -> Unit, +) : Snapshotter { + override fun snapshot(name: String?) { + paparazzi.snapshot(name, widget) + } +} diff --git a/redwood-lazylayout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiLazyListTest.kt b/redwood-lazylayout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiLazyListTest.kt index 1efec7bdc8..b430cb82fa 100644 --- a/redwood-lazylayout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiLazyListTest.kt +++ b/redwood-lazylayout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiLazyListTest.kt @@ -99,11 +99,7 @@ class ComposeUiLazyListTest( } } - override fun verifySnapshot(widget: @Composable () -> Unit, name: String?) { - paparazzi.snapshot(name) { - widget() - } - } + override fun snapshotter(widget: @Composable () -> Unit) = ComposeSnapshotter(paparazzi, widget) class ComposeTestFlexContainer private constructor( private val delegate: ComposeUiLazyList, diff --git a/redwood-lazylayout-uiview/src/commonTest/kotlin/app/cash/redwood/lazylayout/uiview/UIViewLazyListAsFlexContainerTest.kt b/redwood-lazylayout-uiview/src/commonTest/kotlin/app/cash/redwood/lazylayout/uiview/UIViewLazyListAsFlexContainerTest.kt index 2886994d13..e1f3acea1a 100644 --- a/redwood-lazylayout-uiview/src/commonTest/kotlin/app/cash/redwood/lazylayout/uiview/UIViewLazyListAsFlexContainerTest.kt +++ b/redwood-lazylayout-uiview/src/commonTest/kotlin/app/cash/redwood/lazylayout/uiview/UIViewLazyListAsFlexContainerTest.kt @@ -29,9 +29,6 @@ import app.cash.redwood.widget.ChangeListener import app.cash.redwood.widget.ResizableWidget import app.cash.redwood.widget.Widget import app.cash.redwood.yoga.FlexDirection -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.DurationUnit -import platform.CoreGraphics.CGRectMake import platform.UIKit.UILabel import platform.UIKit.UIView @@ -68,23 +65,7 @@ class UIViewLazyListAsFlexContainerTest( } } - override fun verifySnapshot(widget: UIView, name: String?) { - val screenSize = CGRectMake(0.0, 0.0, 390.0, 844.0) // iPhone 14. - widget.setFrame(screenSize) - - // Snapshot the container on a white background. - val frame = UIView().apply { - backgroundColor = platform.UIKit.UIColor.whiteColor - setFrame(screenSize) - addSubview(widget) - layoutIfNeeded() - } - - // Unfortunately even with animations forced off, UITableView's animation system breaks - // synchronous snapshots. The simplest workaround is to delay snapshots one frame. - callback.verifySnapshot(frame, name, delay = 1.milliseconds.toDouble(DurationUnit.SECONDS)) - widget.removeFromSuperview() - } + override fun snapshotter(widget: UIView) = UIViewSnapshotter.framed(callback, widget) class ViewTestFlexContainer private constructor( private val delegate: LazyList, diff --git a/redwood-lazylayout-uiview/src/commonTest/kotlin/app/cash/redwood/lazylayout/uiview/UIViewSnapshotter.kt b/redwood-lazylayout-uiview/src/commonTest/kotlin/app/cash/redwood/lazylayout/uiview/UIViewSnapshotter.kt new file mode 100644 index 0000000000..6da560e793 --- /dev/null +++ b/redwood-lazylayout-uiview/src/commonTest/kotlin/app/cash/redwood/lazylayout/uiview/UIViewSnapshotter.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package app.cash.redwood.lazylayout.uiview + +import app.cash.redwood.layout.Snapshotter +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.DurationUnit +import platform.CoreGraphics.CGRectMake +import platform.UIKit.UIColor +import platform.UIKit.UIView + +/** Snapshot the subject on a white background. */ +class UIViewSnapshotter( + private val callback: UIViewSnapshotCallback, + private val subject: UIView, +) : Snapshotter { + + override fun snapshot(name: String?) { + layoutSubject() + + // Unfortunately even with animations forced off, UITableView's animation system breaks + // synchronous snapshots. The simplest workaround is to delay snapshots one frame. + callback.verifySnapshot(subject, name, delay = 1.milliseconds.toDouble(DurationUnit.SECONDS)) + } + + /** Do layout without taking a snapshot. */ + fun layoutSubject() { + subject.layoutIfNeeded() + } + + companion object { + fun framed( + callback: UIViewSnapshotCallback, + widget: UIView, + ): UIViewSnapshotter { + val frame = UIView() + .apply { + val screenSize = CGRectMake(0.0, 0.0, 390.0, 844.0) // iPhone 14. + + backgroundColor = UIColor.whiteColor + setFrame(screenSize) + + widget.setFrame(screenSize) + addSubview(widget) + } + return UIViewSnapshotter(callback, frame) + } + } +} diff --git a/redwood-lazylayout-view/src/test/kotlin/app/cash/redwood/lazylayout/view/ViewLazyListAsFlexContainerTest.kt b/redwood-lazylayout-view/src/test/kotlin/app/cash/redwood/lazylayout/view/ViewLazyListAsFlexContainerTest.kt index fe708d4aad..fdce4c27e1 100644 --- a/redwood-lazylayout-view/src/test/kotlin/app/cash/redwood/lazylayout/view/ViewLazyListAsFlexContainerTest.kt +++ b/redwood-lazylayout-view/src/test/kotlin/app/cash/redwood/lazylayout/view/ViewLazyListAsFlexContainerTest.kt @@ -87,9 +87,7 @@ class ViewLazyListAsFlexContainerTest( } } - override fun verifySnapshot(widget: View, name: String?) { - paparazzi.snapshot(widget, name) - } + override fun snapshotter(widget: View) = ViewSnapshotter(paparazzi, widget) class ViewTestFlexContainer private constructor( private val delegate: ViewLazyList, diff --git a/redwood-lazylayout-view/src/test/kotlin/app/cash/redwood/lazylayout/view/ViewSnapshotter.kt b/redwood-lazylayout-view/src/test/kotlin/app/cash/redwood/lazylayout/view/ViewSnapshotter.kt new file mode 100644 index 0000000000..12ae196e15 --- /dev/null +++ b/redwood-lazylayout-view/src/test/kotlin/app/cash/redwood/lazylayout/view/ViewSnapshotter.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package app.cash.redwood.lazylayout.view + +import android.view.View +import app.cash.paparazzi.Paparazzi +import app.cash.redwood.layout.Snapshotter + +class ViewSnapshotter( + private val paparazzi: Paparazzi, + private val view: View, +) : Snapshotter { + override fun snapshot(name: String?) { + paparazzi.snapshot(view = view, name = name) + } +}