diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 8f3e9d8b97..8d2f3b646a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -85,6 +85,8 @@ jobs: - run: xcodebuild -project redwood-layout-uiview/RedwoodLayoutUIViewTests.xcodeproj -scheme RedwoodLayoutUIViewTests -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' test + - run: xcodebuild -project redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests.xcodeproj -scheme RedwoodLazylayoutUIViewTests -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' test + - uses: actions/upload-artifact@v4 if: ${{ always() }} with: diff --git a/redwood-layout-uiview/RedwoodLayoutUIViewTests/SnapshotTestingCallback.swift b/redwood-layout-uiview/RedwoodLayoutUIViewTests/SnapshotTestingCallback.swift index 41ab23050e..8700a458bc 100644 --- a/redwood-layout-uiview/RedwoodLayoutUIViewTests/SnapshotTestingCallback.swift +++ b/redwood-layout-uiview/RedwoodLayoutUIViewTests/SnapshotTestingCallback.swift @@ -11,9 +11,9 @@ final class SnapshotTestingCallback : UIViewSnapshotCallback { self.fileName = fileName } - func verifySnapshot(view: UIView, name: String?) { + func verifySnapshot(view: UIView, name: String?, delay: TimeInterval = 0.0) { // Set `record` to true to generate new snapshots. Be sure to revert that before committing! // Note that tests always fail when `record` is true. - assertSnapshot(of: view, as: .image, named: name, record: false, file: fileName, testName: testName) + assertSnapshot(of: view, as: .wait(for: delay, on: .image), named: name, record: false, file: fileName, testName: testName) } } diff --git a/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewSnapshotCallback.kt b/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewSnapshotCallback.kt index 2dd8d84767..06dac31412 100644 --- a/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewSnapshotCallback.kt +++ b/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewSnapshotCallback.kt @@ -18,5 +18,5 @@ package app.cash.redwood.layout.uiview import platform.UIKit.UIView interface UIViewSnapshotCallback { - fun verifySnapshot(view: UIView, name: String?) + fun verifySnapshot(view: UIView, name: String?, delay: Double = 0.0) } diff --git a/redwood-lazylayout-shared-test/build.gradle b/redwood-lazylayout-shared-test/build.gradle new file mode 100644 index 0000000000..bb78f25513 --- /dev/null +++ b/redwood-lazylayout-shared-test/build.gradle @@ -0,0 +1,28 @@ +import static app.cash.redwood.buildsupport.TargetGroup.ToolkitAllWithoutAndroid + +redwoodBuild { + targets(ToolkitAllWithoutAndroid) +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api projects.redwoodLazylayoutApi + api projects.redwoodLazylayoutWidget + api projects.redwoodRuntime + api projects.redwoodWidget + api projects.redwoodYoga + api libs.kotlin.test + } + } + jvmMain { + dependencies { + // The kotlin.test library provides JVM variants for multiple testing frameworks. When used + // as a test dependency this selection is transparent. But since we are publishing a library + // we need to select one ourselves at compilation time. + api libs.kotlin.test.junit + } + } + } +} diff --git a/redwood-lazylayout-shared-test/src/commonMain/kotlin/app/cash/redwood/lazylayout/AbstractLazyListTest.kt b/redwood-lazylayout-shared-test/src/commonMain/kotlin/app/cash/redwood/lazylayout/AbstractLazyListTest.kt new file mode 100644 index 0000000000..68857549c0 --- /dev/null +++ b/redwood-lazylayout-shared-test/src/commonMain/kotlin/app/cash/redwood/lazylayout/AbstractLazyListTest.kt @@ -0,0 +1,134 @@ +/* + * 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 + +import app.cash.redwood.Modifier +import app.cash.redwood.layout.api.Constraint +import app.cash.redwood.layout.api.CrossAxisAlignment +import app.cash.redwood.lazylayout.api.ScrollItemIndex +import app.cash.redwood.lazylayout.widget.LazyList +import app.cash.redwood.ui.Margin +import app.cash.redwood.ui.dp +import app.cash.redwood.widget.ChangeListener +import app.cash.redwood.widget.Widget +import kotlin.test.Test + +abstract class AbstractLazyListTest { + abstract fun text(): Text + + private fun coloredText( + modifier: Modifier = Modifier, + text: String, + backgroundColor: Int = Green, + ) = text().apply { + this.modifier = modifier + text(text) + bgColor(backgroundColor) + } + + abstract fun lazyList( + backgroundColor: Int = argb(51, 0, 0, 255), + ): LazyList + + private fun defaultLazyList(): LazyList { + val result = lazyList() + for (i in 0 until 10) { + result.placeholder.insert(i, coloredText(text = "...")) + } + result.isVertical(true) + result.itemsBefore(0) + result.itemsAfter(0) + result.width(Constraint.Fill) + result.height(Constraint.Fill) + result.margin(Margin(all = 0.dp)) + result.crossAxisAlignment(CrossAxisAlignment.Stretch) + result.scrollItemIndex(ScrollItemIndex(id = 0, index = 0)) + return result + } + + abstract fun verifySnapshot( + container: Widget, + name: String? = null, + ) + + @Test fun testHappyPath() { + val lazyList = defaultLazyList() + + for ((index, value) in movies.take(5).withIndex()) { + lazyList.items.insert(index, coloredText(text = value)) + } + (lazyList as? ChangeListener)?.onEndChanges() + + verifySnapshot(lazyList) + } + + @Test fun testPlaceholderToLoadedAndLoadedToPlaceholder() { + val lazyList = defaultLazyList() + + (lazyList as? ChangeListener)?.onEndChanges() + verifySnapshot(lazyList, "0 empty") + + lazyList.itemsBefore(0) + lazyList.itemsAfter(10) + (lazyList as? ChangeListener)?.onEndChanges() + verifySnapshot(lazyList, "1 placeholders") + + lazyList.itemsBefore(0) + lazyList.itemsAfter(0) + for ((index, value) in movies.take(10).withIndex()) { + lazyList.items.insert(index, coloredText(text = value)) + } + (lazyList as? ChangeListener)?.onEndChanges() + verifySnapshot(lazyList, "2 loaded") + + lazyList.itemsBefore(0) + lazyList.itemsAfter(10) + lazyList.items.remove(0, 10) + (lazyList as? ChangeListener)?.onEndChanges() + verifySnapshot(lazyList, "3 placeholders") + + lazyList.itemsBefore(0) + lazyList.itemsAfter(0) + (lazyList as? ChangeListener)?.onEndChanges() + verifySnapshot(lazyList, "4 empty") + } + + @Test fun testPlaceholdersExhausted() { + val lazyList = defaultLazyList() + + lazyList.itemsBefore(11) + for ((index, value) in movies.take(1).withIndex()) { + lazyList.items.insert(index, coloredText(text = value)) + } + (lazyList as? ChangeListener)?.onEndChanges() + verifySnapshot(lazyList) + } +} + +private val movies = listOf( + "The Godfather", + "The Dark Knight", + "12 Angry Men", + "Schindler's List", + "Pulp Fiction", + "Forrest Gump", + "Fight Club", + "Inception", + "The Matrix", + "Goodfellas", + "Se7en", + "Seven Samurai", +) diff --git a/redwood-lazylayout-shared-test/src/commonMain/kotlin/app/cash/redwood/lazylayout/utils.common.kt b/redwood-lazylayout-shared-test/src/commonMain/kotlin/app/cash/redwood/lazylayout/utils.common.kt new file mode 100644 index 0000000000..9ec3b24171 --- /dev/null +++ b/redwood-lazylayout-shared-test/src/commonMain/kotlin/app/cash/redwood/lazylayout/utils.common.kt @@ -0,0 +1,36 @@ +/* + * 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. + */ +@file:Suppress("ktlint:standard:property-naming") + +package app.cash.redwood.lazylayout + +import app.cash.redwood.widget.Widget + +const val Green: Int = 0xff00ff00.toInt() + +fun argb( + alpha: Int, + red: Int, + green: Int, + blue: Int, +): Int { + return (alpha shl 24) or (red shl 16) or (green shl 8) or (blue) +} + +interface Text : Widget { + fun text(text: String) + fun bgColor(color: Int) +} diff --git a/redwood-lazylayout-shared-test/src/iosMain/kotlin/app/cash/redwood/lazylayout/utils.kt b/redwood-lazylayout-shared-test/src/iosMain/kotlin/app/cash/redwood/lazylayout/utils.kt new file mode 100644 index 0000000000..3fb298616a --- /dev/null +++ b/redwood-lazylayout-shared-test/src/iosMain/kotlin/app/cash/redwood/lazylayout/utils.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.lazylayout + +import platform.UIKit.UIColor + +fun Int.toUIColor(): UIColor { + return UIColor( + red = ((this shr 16) and 0xff) / 255.0, + green = ((this shr 8) and 0xff) / 255.0, + blue = (this and 0xff) / 255.0, + alpha = ((this shr 24) and 0xff) / 255.0, + ) +} diff --git a/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests.xcodeproj/project.pbxproj b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..9de94bf2c2 --- /dev/null +++ b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests.xcodeproj/project.pbxproj @@ -0,0 +1,420 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 102F1C8C2C7D18C700890D9F /* UIViewLazyListTestHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 102F1C8B2C7D18C700890D9F /* UIViewLazyListTestHost.swift */; }; + CB408CED2AC6549F00E1BA00 /* KotlinHostingXCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB408CEC2AC6549F00E1BA00 /* KotlinHostingXCTestCase.swift */; }; + CB408CF72AC657EF00E1BA00 /* KotlinHostingXCTestCaseHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = CB408CF62AC657EF00E1BA00 /* KotlinHostingXCTestCaseHelper.m */; }; + CB8A21EF2AC6647F00C104C2 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = CB8A21EE2AC6647F00C104C2 /* SnapshotTesting */; }; + CB9729D32AD82D0C00804E94 /* SnapshotTestingCallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB9729D22AD82D0C00804E94 /* SnapshotTestingCallback.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 102F1C8B2C7D18C700890D9F /* UIViewLazyListTestHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewLazyListTestHost.swift; sourceTree = ""; }; + 63E90CF521FEBBB700449E04 /* RedwoodLazylayoutUIViewTestKt.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RedwoodLazylayoutUIViewTestKt.framework; path = "build/xcode-frameworks/RedwoodLazylayoutUIViewTestKt.framework"; sourceTree = ""; }; + CB408CE52AC64CEF00E1BA00 /* RedwoodLazylayoutUIViewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RedwoodLazylayoutUIViewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + CB408CEC2AC6549F00E1BA00 /* KotlinHostingXCTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KotlinHostingXCTestCase.swift; sourceTree = ""; }; + CB408CF52AC657EF00E1BA00 /* RedwoodLazylayoutUIViewTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RedwoodLazylayoutUIViewTests-Bridging-Header.h"; sourceTree = ""; }; + CB408CF62AC657EF00E1BA00 /* KotlinHostingXCTestCaseHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KotlinHostingXCTestCaseHelper.m; sourceTree = ""; }; + CB408CF82AC6581100E1BA00 /* KotlinHostingXCTestCaseHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KotlinHostingXCTestCaseHelper.h; sourceTree = ""; }; + CB9729D22AD82D0C00804E94 /* SnapshotTestingCallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotTestingCallback.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + CB408CE22AC64CEF00E1BA00 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CB8A21EF2AC6647F00C104C2 /* SnapshotTesting in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 00D5E68C2AAF3EBD00692213 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 635661C821F12B7D00DD7240 = { + isa = PBXGroup; + children = ( + CB408CE62AC64CEF00E1BA00 /* RedwoodLazylayoutUIViewTests */, + 635661D221F12B7E00DD7240 /* Products */, + 00D5E68C2AAF3EBD00692213 /* Frameworks */, + 63E90CF521FEBBB700449E04 /* RedwoodLazylayoutUIViewTestKt.framework */, + ); + sourceTree = ""; + }; + 635661D221F12B7E00DD7240 /* Products */ = { + isa = PBXGroup; + children = ( + CB408CE52AC64CEF00E1BA00 /* RedwoodLazylayoutUIViewTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + CB408CE62AC64CEF00E1BA00 /* RedwoodLazylayoutUIViewTests */ = { + isa = PBXGroup; + children = ( + CB9729D22AD82D0C00804E94 /* SnapshotTestingCallback.swift */, + CB408CEC2AC6549F00E1BA00 /* KotlinHostingXCTestCase.swift */, + CB408CF62AC657EF00E1BA00 /* KotlinHostingXCTestCaseHelper.m */, + CB408CF52AC657EF00E1BA00 /* RedwoodLazylayoutUIViewTests-Bridging-Header.h */, + CB408CF82AC6581100E1BA00 /* KotlinHostingXCTestCaseHelper.h */, + 102F1C8B2C7D18C700890D9F /* UIViewLazyListTestHost.swift */, + ); + path = RedwoodLazylayoutUIViewTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + CB408CE42AC64CEF00E1BA00 /* RedwoodLazylayoutUIViewTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = CB408CE92AC64CEF00E1BA00 /* Build configuration list for PBXNativeTarget "RedwoodLazylayoutUIViewTests" */; + buildPhases = ( + 00DD12E22AAF46C500CA3FD3 /* ShellScript */, + CB408CE12AC64CEF00E1BA00 /* Sources */, + CB408CE22AC64CEF00E1BA00 /* Frameworks */, + CB408CE32AC64CEF00E1BA00 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RedwoodLazylayoutUIViewTests; + packageProductDependencies = ( + CB8A21EE2AC6647F00C104C2 /* SnapshotTesting */, + ); + productName = RedwoodLazylayoutUIViewTests; + productReference = CB408CE52AC64CEF00E1BA00 /* RedwoodLazylayoutUIViewTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 635661C921F12B7D00DD7240 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1420; + LastUpgradeCheck = 1220; + ORGANIZATIONNAME = "Square Inc"; + TargetAttributes = { + CB408CE42AC64CEF00E1BA00 = { + CreatedOnToolsVersion = 14.2; + LastSwiftMigration = 1420; + }; + }; + }; + buildConfigurationList = 635661CC21F12B7D00DD7240 /* Build configuration list for PBXProject "RedwoodLazylayoutUIViewTests" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 635661C821F12B7D00DD7240; + packageReferences = ( + CB8A21ED2AC6647F00C104C2 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */, + ); + productRefGroup = 635661D221F12B7E00DD7240 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + CB408CE42AC64CEF00E1BA00 /* RedwoodLazylayoutUIViewTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + CB408CE32AC64CEF00E1BA00 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD12E22AAF46C500CA3FD3 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cd \"$SRCROOT/..\"\n./gradlew :redwood-lazylayout-uiview:embedAndSignAppleFrameworkForXcode\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + CB408CE12AC64CEF00E1BA00 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CB408CED2AC6549F00E1BA00 /* KotlinHostingXCTestCase.swift in Sources */, + CB408CF72AC657EF00E1BA00 /* KotlinHostingXCTestCaseHelper.m in Sources */, + CB9729D32AD82D0C00804E94 /* SnapshotTestingCallback.swift in Sources */, + 102F1C8C2C7D18C700890D9F /* UIViewLazyListTestHost.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 635661E121F12B8000DD7240 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGNING_ALLOWED = NO; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 635661E221F12B8000DD7240 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGNING_ALLOWED = NO; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + CB408CEA2AC64CEF00E1BA00 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + FRAMEWORK_SEARCH_PATHS = ( + "$(SRCROOT)/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + "$(PROJECT_DIR)/build/xcode-frameworks", + ); + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-l\"c++\"", + "-framework", + "\"RedwoodLazylayoutUIViewTestKt\"", + "-ObjC", + "-framework", + RedwoodLazylayoutUIViewTestKt, + ); + PRODUCT_BUNDLE_IDENTIFIER = app.cash.redwood.layout.uiview.testing; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OBJC_BRIDGING_HEADER = "RedwoodLazylayoutUIViewTests/RedwoodLazylayoutUIViewTests-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + CB408CEB2AC64CEF00E1BA00 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + FRAMEWORK_SEARCH_PATHS = ( + "$(SRCROOT)/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + "$(PROJECT_DIR)/build/xcode-frameworks", + ); + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-l\"c++\"", + "-framework", + "\"RedwoodLazylayoutUIViewTestKt\"", + "-ObjC", + "-framework", + RedwoodLazylayoutUIViewTestKt, + ); + PRODUCT_BUNDLE_IDENTIFIER = app.cash.redwood.layout.uiview.testing; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OBJC_BRIDGING_HEADER = "RedwoodLazylayoutUIViewTests/RedwoodLazylayoutUIViewTests-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 635661CC21F12B7D00DD7240 /* Build configuration list for PBXProject "RedwoodLazylayoutUIViewTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 635661E121F12B8000DD7240 /* Debug */, + 635661E221F12B8000DD7240 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CB408CE92AC64CEF00E1BA00 /* Build configuration list for PBXNativeTarget "RedwoodLazylayoutUIViewTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CB408CEA2AC64CEF00E1BA00 /* Debug */, + CB408CEB2AC64CEF00E1BA00 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + CB8A21ED2AC6647F00C104C2 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-snapshot-testing"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + CB8A21EE2AC6647F00C104C2 /* SnapshotTesting */ = { + isa = XCSwiftPackageProductDependency; + package = CB8A21ED2AC6647F00C104C2 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */; + productName = SnapshotTesting; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 635661C921F12B7D00DD7240 /* Project object */; +} diff --git a/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests.xcodeproj/xcshareddata/xcschemes/RedwoodLazylayoutUIViewTests.xcscheme b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests.xcodeproj/xcshareddata/xcschemes/RedwoodLazylayoutUIViewTests.xcscheme new file mode 100644 index 0000000000..71d5321ebb --- /dev/null +++ b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests.xcodeproj/xcshareddata/xcschemes/RedwoodLazylayoutUIViewTests.xcscheme @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/KotlinHostingXCTestCase.swift b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/KotlinHostingXCTestCase.swift new file mode 100644 index 0000000000..c0382c8a5c --- /dev/null +++ b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/KotlinHostingXCTestCase.swift @@ -0,0 +1,63 @@ +import Foundation +import XCTest + +/** + * An abstract base class for hosting Kotlin test classes in Swift projects. + * + * Subclasses must override `initTest(name:)` to create instances of `KotlinType`. + */ +open class KotlinHostingXCTestCase: KotlinHostingXCTestCaseHelper where KotlinTest: NSObject { + /** + * Create an instance of `KotlinTest` for invoking test method `name`. + */ + open class func initTest(name: String) -> KotlinTest { + fatalError("Missing override for initTest(name:)") + } + + public override class func createTestMethods() { + var targetClass: AnyClass = KotlinTest.self + while (targetClass != NSObject.self) { + var methodCount: UInt32 = 0 + let methodList = class_copyMethodList(targetClass, &methodCount) + if let methodList = methodList { + defer { free(methodList) } + for i in 0.. Void = { + if target.responds(to: #selector(setUp)) { + target.perform(#selector(setUp)) + } + defer { + if target.responds(to: #selector(tearDown)) { + target.perform(#selector(tearDown)) + } + } + + target.perform(selector) + } + let implementation = imp_implementationWithBlock(block) + class_addMethod(self, selector, implementation, "v@:") + } + + public override class var defaultTestSuite: XCTestSuite { + createTestMethods() + return super.defaultTestSuite + } +} diff --git a/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/KotlinHostingXCTestCaseHelper.h b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/KotlinHostingXCTestCaseHelper.h new file mode 100644 index 0000000000..7e4622d582 --- /dev/null +++ b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/KotlinHostingXCTestCaseHelper.h @@ -0,0 +1,20 @@ +// Created by Michał Laskowski on 10/02/2020. +// Copyright © 2020 Michał Laskowski. All rights reserved. +// Derived from https://github.com/michallaskowski/kuiks/blob/c8500df2a55fe031a1bcf546c771c3d9f30dbf90/NativeTestBase/ObjC/TestBaseForSelector.h +// Licensed as Apache-2.0. + +#ifndef TestBase_h +#define TestBase_h +@import XCTest; + +NS_ASSUME_NONNULL_BEGIN + +@interface KotlinHostingXCTestCaseHelper : XCTestCase + ++(void)createTestMethods; + +@end + +NS_ASSUME_NONNULL_END + +#endif /* TestBase_h */ diff --git a/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/KotlinHostingXCTestCaseHelper.m b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/KotlinHostingXCTestCaseHelper.m new file mode 100644 index 0000000000..f7991ed221 --- /dev/null +++ b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/KotlinHostingXCTestCaseHelper.m @@ -0,0 +1,25 @@ +// Created by Michał Laskowski on 10/02/2020. +// Copyright © 2020 Michał Laskowski. All rights reserved. +// Derived from https://github.com/michallaskowski/kuiks/blob/c8500df2a55fe031a1bcf546c771c3d9f30dbf90/NativeTestBase/ObjC/TestBaseForSelector.m +// Licensed as Apache-2.0. + +#import +#include "KotlinHostingXCTestCaseHelper.h" +#import "objc/runtime.h" + +@implementation KotlinHostingXCTestCaseHelper + ++(void)createTestMethods { + [NSException raise:@"NotImplemented" format:@"Subclasses must implement a valid createTestMethods method"]; +} + +/** + This is overriden in order to be able to trigger tests from Test Navigator panel. Method can not be overriden in Swift, because it needs also + to override `testCaseWithInvocation:`. And using NSInvocation is not possible in Swift. Hence this Obj-C TestBase class, and this method. + */ ++(instancetype)testCaseWithSelector:(SEL)selector { + [self createTestMethods]; + return [super testCaseWithSelector:selector]; +} + +@end diff --git a/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/RedwoodLazylayoutUIViewTests-Bridging-Header.h b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/RedwoodLazylayoutUIViewTests-Bridging-Header.h new file mode 100644 index 0000000000..832bf76ca4 --- /dev/null +++ b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/RedwoodLazylayoutUIViewTests-Bridging-Header.h @@ -0,0 +1 @@ +#include "KotlinHostingXCTestCaseHelper.h" diff --git a/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/SnapshotTestingCallback.swift b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/SnapshotTestingCallback.swift new file mode 100644 index 0000000000..802de464f5 --- /dev/null +++ b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/SnapshotTestingCallback.swift @@ -0,0 +1,19 @@ +import RedwoodLazylayoutUIViewTestKt +import SnapshotTesting +import UIKit + +final class SnapshotTestingCallback : UIViewSnapshotCallback { + private let testName: String + private let fileName: StaticString + + init(named testName: String, _ fileName: StaticString = #file) { + self.testName = testName + self.fileName = fileName + } + + func verifySnapshot(view: UIView, name: String?, delay: TimeInterval = 0.0) { + // Set `record` to true to generate new snapshots. Be sure to revert that before committing! + // Note that tests always fail when `record` is true. + assertSnapshot(of: view, as: .wait(for: delay, on: .image), named: name, record: false, file: fileName, testName: testName) + } +} diff --git a/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/UIViewLazyListTestHost.swift b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/UIViewLazyListTestHost.swift new file mode 100644 index 0000000000..c2c00536a6 --- /dev/null +++ b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/UIViewLazyListTestHost.swift @@ -0,0 +1,8 @@ +import RedwoodLazylayoutUIViewTestKt +import UIKit + +final class UIViewLazyListTestHost: KotlinHostingXCTestCase { + override class func initTest(name: String) -> UIViewLazyListTest { + return UIViewLazyListTest(callback: SnapshotTestingCallback(named: name)) + } +} diff --git a/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testHappyPath.1.png b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testHappyPath.1.png new file mode 100644 index 0000000000..3c110a255c Binary files /dev/null and b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testHappyPath.1.png differ diff --git a/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testPlaceholderToLoadedAndLoadedToPlaceholder.0-empty.png b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testPlaceholderToLoadedAndLoadedToPlaceholder.0-empty.png new file mode 100644 index 0000000000..c266f4428b Binary files /dev/null and b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testPlaceholderToLoadedAndLoadedToPlaceholder.0-empty.png differ diff --git a/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testPlaceholderToLoadedAndLoadedToPlaceholder.1-placeholders.png b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testPlaceholderToLoadedAndLoadedToPlaceholder.1-placeholders.png new file mode 100644 index 0000000000..a536f1382c Binary files /dev/null and b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testPlaceholderToLoadedAndLoadedToPlaceholder.1-placeholders.png differ diff --git a/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testPlaceholderToLoadedAndLoadedToPlaceholder.2-loaded.png b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testPlaceholderToLoadedAndLoadedToPlaceholder.2-loaded.png new file mode 100644 index 0000000000..e3ff28fc2c Binary files /dev/null and b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testPlaceholderToLoadedAndLoadedToPlaceholder.2-loaded.png differ diff --git a/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testPlaceholderToLoadedAndLoadedToPlaceholder.3-placeholders.png b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testPlaceholderToLoadedAndLoadedToPlaceholder.3-placeholders.png new file mode 100644 index 0000000000..a536f1382c Binary files /dev/null and b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testPlaceholderToLoadedAndLoadedToPlaceholder.3-placeholders.png differ diff --git a/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testPlaceholderToLoadedAndLoadedToPlaceholder.4-empty.png b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testPlaceholderToLoadedAndLoadedToPlaceholder.4-empty.png new file mode 100644 index 0000000000..c266f4428b Binary files /dev/null and b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testPlaceholderToLoadedAndLoadedToPlaceholder.4-empty.png differ diff --git a/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testPlaceholdersExhausted.1.png b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testPlaceholdersExhausted.1.png new file mode 100644 index 0000000000..46b689bb56 Binary files /dev/null and b/redwood-lazylayout-uiview/RedwoodLazylayoutUIViewTests/__Snapshots__/UIViewLazyListTestHost/testPlaceholdersExhausted.1.png differ diff --git a/redwood-lazylayout-uiview/build.gradle b/redwood-lazylayout-uiview/build.gradle index 8609c29661..65bd984459 100644 --- a/redwood-lazylayout-uiview/build.gradle +++ b/redwood-lazylayout-uiview/build.gradle @@ -1,4 +1,5 @@ import static app.cash.redwood.buildsupport.TargetGroup.ToolkitIos +import static org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.common redwoodBuild { targets(ToolkitIos) @@ -6,6 +7,15 @@ redwoodBuild { } kotlin { + targets.configureEach { target -> + if (target.platformType != common) { + target.binaries.framework { + compilation = target.compilations.test + baseName = 'RedwoodLazylayoutUIViewTestKt' + } + } + } + sourceSets { commonMain { dependencies { @@ -13,5 +23,11 @@ kotlin { implementation projects.redwoodWidgetCompose } } + commonTest { + dependencies { + api projects.redwoodLazylayoutSharedTest + api libs.kotlin.test + } + } } } diff --git a/redwood-lazylayout-uiview/src/commonTest/kotlin/app/cash/redwood/lazylayout/uiview/UIViewLazyListTest.kt b/redwood-lazylayout-uiview/src/commonTest/kotlin/app/cash/redwood/lazylayout/uiview/UIViewLazyListTest.kt new file mode 100644 index 0000000000..1b4661c8bc --- /dev/null +++ b/redwood-lazylayout-uiview/src/commonTest/kotlin/app/cash/redwood/lazylayout/uiview/UIViewLazyListTest.kt @@ -0,0 +1,52 @@ +/* + * 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.lazylayout.AbstractLazyListTest +import app.cash.redwood.widget.Widget +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.DurationUnit +import platform.CoreGraphics.CGRectMake +import platform.UIKit.UIColor +import platform.UIKit.UIView + +class UIViewLazyListTest( + private val callback: UIViewSnapshotCallback, +) : AbstractLazyListTest() { + private val widgetFactory = UIViewRedwoodLazyLayoutWidgetFactory() + + override fun text() = UIViewText() + + override fun lazyList(backgroundColor: Int) = widgetFactory.LazyList() + + override fun verifySnapshot(container: Widget, name: String?) { + val screenSize = CGRectMake(0.0, 0.0, 390.0, 844.0) // iPhone 14. + container.value.setFrame(screenSize) + + // Snapshot the container on a white background. + val frame = UIView().apply { + backgroundColor = UIColor.whiteColor + setFrame(screenSize) + addSubview(container.value) + 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)) + container.value.removeFromSuperview() + } +} diff --git a/redwood-lazylayout-uiview/src/commonTest/kotlin/app/cash/redwood/lazylayout/uiview/UIViewSnapshotCallback.kt b/redwood-lazylayout-uiview/src/commonTest/kotlin/app/cash/redwood/lazylayout/uiview/UIViewSnapshotCallback.kt new file mode 100644 index 0000000000..e2e544c027 --- /dev/null +++ b/redwood-lazylayout-uiview/src/commonTest/kotlin/app/cash/redwood/lazylayout/uiview/UIViewSnapshotCallback.kt @@ -0,0 +1,22 @@ +/* + * 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 platform.UIKit.UIView + +interface UIViewSnapshotCallback { + fun verifySnapshot(view: UIView, name: String?, delay: Double = 0.0) +} diff --git a/redwood-lazylayout-uiview/src/commonTest/kotlin/app/cash/redwood/lazylayout/uiview/testWidgets.kt b/redwood-lazylayout-uiview/src/commonTest/kotlin/app/cash/redwood/lazylayout/uiview/testWidgets.kt new file mode 100644 index 0000000000..4c7c2b2d30 --- /dev/null +++ b/redwood-lazylayout-uiview/src/commonTest/kotlin/app/cash/redwood/lazylayout/uiview/testWidgets.kt @@ -0,0 +1,38 @@ +/* + * 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.Modifier +import app.cash.redwood.lazylayout.Text +import app.cash.redwood.lazylayout.toUIColor +import platform.UIKit.UILabel +import platform.UIKit.UIView + +class UIViewText : Text { + override val value = UILabel().apply { + numberOfLines = 0 + textColor = platform.UIKit.UIColor.blackColor + } + override var modifier: Modifier = Modifier + + override fun text(text: String) { + value.text = text + } + + override fun bgColor(color: Int) { + value.backgroundColor = color.toUIColor() + } +} diff --git a/redwood-lazylayout-view/build.gradle b/redwood-lazylayout-view/build.gradle index 3a566918f6..a8c1145896 100644 --- a/redwood-lazylayout-view/build.gradle +++ b/redwood-lazylayout-view/build.gradle @@ -15,6 +15,7 @@ dependencies { implementation libs.androidx.swipeRefreshLayout implementation libs.kotlinx.coroutines.android testImplementation projects.redwoodLayoutSharedTest + testImplementation projects.redwoodLazylayoutSharedTest testImplementation projects.redwoodLayoutView testImplementation libs.testParameterInjector } diff --git a/redwood-lazylayout-view/src/test/kotlin/app/cash/redwood/lazylayout/view/ViewLazyListTest.kt b/redwood-lazylayout-view/src/test/kotlin/app/cash/redwood/lazylayout/view/ViewLazyListTest.kt new file mode 100644 index 0000000000..d80e2e0a3e --- /dev/null +++ b/redwood-lazylayout-view/src/test/kotlin/app/cash/redwood/lazylayout/view/ViewLazyListTest.kt @@ -0,0 +1,70 @@ +/* + * 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.graphics.Color +import android.view.View +import android.widget.TextView +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import app.cash.redwood.Modifier +import app.cash.redwood.lazylayout.AbstractLazyListTest +import app.cash.redwood.lazylayout.Text +import app.cash.redwood.lazylayout.widget.LazyList +import app.cash.redwood.widget.Widget +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import org.junit.Rule +import org.junit.runner.RunWith + +@RunWith(TestParameterInjector::class) +class ViewLazyListTest : AbstractLazyListTest() { + + @get:Rule + val paparazzi = Paparazzi( + deviceConfig = DeviceConfig.PIXEL_6, + theme = "android:Theme.Material.Light.NoActionBar", + supportsRtl = true, + ) + + override fun text(): Text { + return object : Text { + override val value = TextView(paparazzi.context).apply { + textSize = 18f + setTextColor(Color.BLACK) + } + + override var modifier: Modifier = Modifier + + override fun text(text: String) { + value.text = text + } + + override fun bgColor(color: Int) { + value.setBackgroundColor(color) + } + } + } + + override fun lazyList(backgroundColor: Int): LazyList { + return ViewLazyList(paparazzi.context).apply { + value.setBackgroundColor(backgroundColor) + } + } + + override fun verifySnapshot(container: Widget, name: String?) { + paparazzi.snapshot(container.value, name) + } +} diff --git a/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testHappyPath.png b/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testHappyPath.png new file mode 100644 index 0000000000..5d0cf11f52 Binary files /dev/null and b/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testHappyPath.png differ diff --git a/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testPlaceholderToLoadedAndLoadedToPlaceholder_0_empty.png b/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testPlaceholderToLoadedAndLoadedToPlaceholder_0_empty.png new file mode 100644 index 0000000000..0975752c4d Binary files /dev/null and b/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testPlaceholderToLoadedAndLoadedToPlaceholder_0_empty.png differ diff --git a/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testPlaceholderToLoadedAndLoadedToPlaceholder_1_placeholders.png b/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testPlaceholderToLoadedAndLoadedToPlaceholder_1_placeholders.png new file mode 100644 index 0000000000..fa85eca659 Binary files /dev/null and b/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testPlaceholderToLoadedAndLoadedToPlaceholder_1_placeholders.png differ diff --git a/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testPlaceholderToLoadedAndLoadedToPlaceholder_2_loaded.png b/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testPlaceholderToLoadedAndLoadedToPlaceholder_2_loaded.png new file mode 100644 index 0000000000..b9d730e80e Binary files /dev/null and b/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testPlaceholderToLoadedAndLoadedToPlaceholder_2_loaded.png differ diff --git a/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testPlaceholderToLoadedAndLoadedToPlaceholder_3_placeholders.png b/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testPlaceholderToLoadedAndLoadedToPlaceholder_3_placeholders.png new file mode 100644 index 0000000000..fa85eca659 Binary files /dev/null and b/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testPlaceholderToLoadedAndLoadedToPlaceholder_3_placeholders.png differ diff --git a/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testPlaceholderToLoadedAndLoadedToPlaceholder_4_empty.png b/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testPlaceholderToLoadedAndLoadedToPlaceholder_4_empty.png new file mode 100644 index 0000000000..0975752c4d Binary files /dev/null and b/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testPlaceholderToLoadedAndLoadedToPlaceholder_4_empty.png differ diff --git a/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testPlaceholdersExhausted.png b/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testPlaceholdersExhausted.png new file mode 100644 index 0000000000..8a26e2b277 Binary files /dev/null and b/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testPlaceholdersExhausted.png differ diff --git a/settings.gradle b/settings.gradle index c988009db4..e033b5a36f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -63,6 +63,7 @@ include ':redwood-lazylayout-compose' include ':redwood-lazylayout-composeui' include ':redwood-lazylayout-dom' include ':redwood-lazylayout-schema' +include ':redwood-lazylayout-shared-test' include ':redwood-lazylayout-testing' include ':redwood-lazylayout-uiview' include ':redwood-lazylayout-view'