Skip to content

Commit 10ae978

Browse files
authored
New snapshot test for LazyList loading and unloading (#2265)
* New snapshot test for LazyList loading and unloading This is in the wrong module as I intend to reuse the test infrastructure in redwood-layout-uiview to test the iOS implementation. * Test UIViewLazyList * Delay snapshots one frame * record: false * Copy-paste snapshot testing infrastructure for lazylist Rather than using the wrong module, or introducing build infrastructure to abstract it up.
1 parent 2bfd69b commit 10ae978

File tree

36 files changed

+1040
-3
lines changed

36 files changed

+1040
-3
lines changed

.github/workflows/build.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ jobs:
8585

8686
- run: xcodebuild -project redwood-layout-uiview/RedwoodLayoutUIViewTests.xcodeproj -scheme RedwoodLayoutUIViewTests -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' test
8787

88+
- run: xcodebuild -project redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests.xcodeproj -scheme RedwoodLazylayoutUIViewTests -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' test
89+
8890
- uses: actions/upload-artifact@v4
8991
if: ${{ always() }}
9092
with:

redwood-layout-uiview/RedwoodLayoutUIViewTests/SnapshotTestingCallback.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ final class SnapshotTestingCallback : UIViewSnapshotCallback {
1111
self.fileName = fileName
1212
}
1313

14-
func verifySnapshot(view: UIView, name: String?) {
14+
func verifySnapshot(view: UIView, name: String?, delay: TimeInterval = 0.0) {
1515
// Set `record` to true to generate new snapshots. Be sure to revert that before committing!
1616
// Note that tests always fail when `record` is true.
17-
assertSnapshot(of: view, as: .image, named: name, record: false, file: fileName, testName: testName)
17+
assertSnapshot(of: view, as: .wait(for: delay, on: .image), named: name, record: false, file: fileName, testName: testName)
1818
}
1919
}

redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewSnapshotCallback.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ package app.cash.redwood.layout.uiview
1818
import platform.UIKit.UIView
1919

2020
interface UIViewSnapshotCallback {
21-
fun verifySnapshot(view: UIView, name: String?)
21+
fun verifySnapshot(view: UIView, name: String?, delay: Double = 0.0)
2222
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import static app.cash.redwood.buildsupport.TargetGroup.ToolkitAllWithoutAndroid
2+
3+
redwoodBuild {
4+
targets(ToolkitAllWithoutAndroid)
5+
}
6+
7+
kotlin {
8+
sourceSets {
9+
commonMain {
10+
dependencies {
11+
api projects.redwoodLazylayoutApi
12+
api projects.redwoodLazylayoutWidget
13+
api projects.redwoodRuntime
14+
api projects.redwoodWidget
15+
api projects.redwoodYoga
16+
api libs.kotlin.test
17+
}
18+
}
19+
jvmMain {
20+
dependencies {
21+
// The kotlin.test library provides JVM variants for multiple testing frameworks. When used
22+
// as a test dependency this selection is transparent. But since we are publishing a library
23+
// we need to select one ourselves at compilation time.
24+
api libs.kotlin.test.junit
25+
}
26+
}
27+
}
28+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* Copyright (C) 2024 Square, Inc.
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+
package app.cash.redwood.lazylayout
17+
18+
import app.cash.redwood.Modifier
19+
import app.cash.redwood.layout.api.Constraint
20+
import app.cash.redwood.layout.api.CrossAxisAlignment
21+
import app.cash.redwood.lazylayout.api.ScrollItemIndex
22+
import app.cash.redwood.lazylayout.widget.LazyList
23+
import app.cash.redwood.ui.Margin
24+
import app.cash.redwood.ui.dp
25+
import app.cash.redwood.widget.ChangeListener
26+
import app.cash.redwood.widget.Widget
27+
import kotlin.test.Test
28+
29+
abstract class AbstractLazyListTest<T : Any> {
30+
abstract fun text(): Text<T>
31+
32+
private fun coloredText(
33+
modifier: Modifier = Modifier,
34+
text: String,
35+
backgroundColor: Int = Green,
36+
) = text().apply {
37+
this.modifier = modifier
38+
text(text)
39+
bgColor(backgroundColor)
40+
}
41+
42+
abstract fun lazyList(
43+
backgroundColor: Int = argb(51, 0, 0, 255),
44+
): LazyList<T>
45+
46+
private fun defaultLazyList(): LazyList<T> {
47+
val result = lazyList()
48+
for (i in 0 until 10) {
49+
result.placeholder.insert(i, coloredText(text = "..."))
50+
}
51+
result.isVertical(true)
52+
result.itemsBefore(0)
53+
result.itemsAfter(0)
54+
result.width(Constraint.Fill)
55+
result.height(Constraint.Fill)
56+
result.margin(Margin(all = 0.dp))
57+
result.crossAxisAlignment(CrossAxisAlignment.Stretch)
58+
result.scrollItemIndex(ScrollItemIndex(id = 0, index = 0))
59+
return result
60+
}
61+
62+
abstract fun verifySnapshot(
63+
container: Widget<T>,
64+
name: String? = null,
65+
)
66+
67+
@Test fun testHappyPath() {
68+
val lazyList = defaultLazyList()
69+
70+
for ((index, value) in movies.take(5).withIndex()) {
71+
lazyList.items.insert(index, coloredText(text = value))
72+
}
73+
(lazyList as? ChangeListener)?.onEndChanges()
74+
75+
verifySnapshot(lazyList)
76+
}
77+
78+
@Test fun testPlaceholderToLoadedAndLoadedToPlaceholder() {
79+
val lazyList = defaultLazyList()
80+
81+
(lazyList as? ChangeListener)?.onEndChanges()
82+
verifySnapshot(lazyList, "0 empty")
83+
84+
lazyList.itemsBefore(0)
85+
lazyList.itemsAfter(10)
86+
(lazyList as? ChangeListener)?.onEndChanges()
87+
verifySnapshot(lazyList, "1 placeholders")
88+
89+
lazyList.itemsBefore(0)
90+
lazyList.itemsAfter(0)
91+
for ((index, value) in movies.take(10).withIndex()) {
92+
lazyList.items.insert(index, coloredText(text = value))
93+
}
94+
(lazyList as? ChangeListener)?.onEndChanges()
95+
verifySnapshot(lazyList, "2 loaded")
96+
97+
lazyList.itemsBefore(0)
98+
lazyList.itemsAfter(10)
99+
lazyList.items.remove(0, 10)
100+
(lazyList as? ChangeListener)?.onEndChanges()
101+
verifySnapshot(lazyList, "3 placeholders")
102+
103+
lazyList.itemsBefore(0)
104+
lazyList.itemsAfter(0)
105+
(lazyList as? ChangeListener)?.onEndChanges()
106+
verifySnapshot(lazyList, "4 empty")
107+
}
108+
109+
@Test fun testPlaceholdersExhausted() {
110+
val lazyList = defaultLazyList()
111+
112+
lazyList.itemsBefore(11)
113+
for ((index, value) in movies.take(1).withIndex()) {
114+
lazyList.items.insert(index, coloredText(text = value))
115+
}
116+
(lazyList as? ChangeListener)?.onEndChanges()
117+
verifySnapshot(lazyList)
118+
}
119+
}
120+
121+
private val movies = listOf(
122+
"The Godfather",
123+
"The Dark Knight",
124+
"12 Angry Men",
125+
"Schindler's List",
126+
"Pulp Fiction",
127+
"Forrest Gump",
128+
"Fight Club",
129+
"Inception",
130+
"The Matrix",
131+
"Goodfellas",
132+
"Se7en",
133+
"Seven Samurai",
134+
)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright (C) 2024 Square, Inc.
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+
@file:Suppress("ktlint:standard:property-naming")
17+
18+
package app.cash.redwood.lazylayout
19+
20+
import app.cash.redwood.widget.Widget
21+
22+
const val Green: Int = 0xff00ff00.toInt()
23+
24+
fun argb(
25+
alpha: Int,
26+
red: Int,
27+
green: Int,
28+
blue: Int,
29+
): Int {
30+
return (alpha shl 24) or (red shl 16) or (green shl 8) or (blue)
31+
}
32+
33+
interface Text<T : Any> : Widget<T> {
34+
fun text(text: String)
35+
fun bgColor(color: Int)
36+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (C) 2024 Square, Inc.
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+
package app.cash.redwood.lazylayout
17+
18+
import platform.UIKit.UIColor
19+
20+
fun Int.toUIColor(): UIColor {
21+
return UIColor(
22+
red = ((this shr 16) and 0xff) / 255.0,
23+
green = ((this shr 8) and 0xff) / 255.0,
24+
blue = (this and 0xff) / 255.0,
25+
alpha = ((this shr 24) and 0xff) / 255.0,
26+
)
27+
}

0 commit comments

Comments
 (0)