diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..159b42e1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +**/screenshots/**/*.png filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 057a8eee..956dc654 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,6 +22,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + lfs: 'true' - name: Setup JDK uses: actions/setup-java@v4 @@ -34,6 +36,15 @@ jobs: - name: Build run: ./gradlew build + - name: Upload reports + Roborazzi outputs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: reports + path: | + **/build/reports/** + **/build/outputs/roborazzi/** + deploy: if: github.ref == 'refs/heads/main' diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index ec63313a..e4a2b5ee 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -20,6 +20,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + lfs: 'true' - name: Setup JDK uses: actions/setup-java@v4 diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index e805548a..ae3f30ae 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index c2f7aa97..eed7e11c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,5 +15,6 @@ plugins { alias(libs.plugins.composeMultiplatform) apply false alias(libs.plugins.mavenpublish) apply false alias(libs.plugins.metalava) apply false + alias(libs.plugins.roborazzi) apply false alias(libs.plugins.dokka) } diff --git a/gradle.properties b/gradle.properties index 92316c96..728c55e7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -27,6 +27,10 @@ systemProp.org.gradle.internal.publish.checksums.insecure=true # Increase timeout when pushing to Sonatype (otherwise we get timeouts) systemProp.org.gradle.internal.http.socketTimeout=120000 +roborazzi.record.filePathStrategy=relativePathFromRoborazziContextOutputDirectory +roborazzi.record.namingStrategy=testClassAndMethod +roborazzi.test.verify=true + ################################## # Publishing ################################## @@ -49,3 +53,4 @@ POM_LICENCE_DIST=repo POM_DEVELOPER_ID=chrisbanes POM_DEVELOPER_NAME=Chris Banes + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 81a8033a..610f7935 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,12 @@ [versions] agp = "8.2.0" +androidx-test-ext-junit = "1.1.5" jetpackcompose-compiler = "1.5.6" compose-multiplatform = "1.5.11" ktlint = "1.0.1" kotlin = "1.9.21" +robolectric = "4.10.3" +roborazzi = "1.9.0-alpha-3" spotless = "6.23.3" [plugins] @@ -16,6 +19,7 @@ composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-mu dokka = { id = "org.jetbrains.dokka", version = "1.9.10" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } metalava = { id = "me.tylerbwong.gradle.metalava", version = "0.3.5" } +roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } mavenpublish = { id = "com.vanniktech.maven.publish", version = "0.26.0" } @@ -25,6 +29,13 @@ androidx-compose-ui = "androidx.compose.ui:ui:1.6.0-beta03" androidx-compose-material3 = "androidx.compose.material3:material3:1.1.2" androidx-core = "androidx.core:core-ktx:1.12.0" androidx-activity-compose = "androidx.activity:activity-compose:1.8.2" +androidx-compose-ui-test-manifest = "androidx.compose.ui:ui-test-manifest:1.5.4" +androidx-test-ext-junit = { module = "androidx.test.ext:junit-ktx", version.ref = "androidx-test-ext-junit" } +robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } +roborazzi-core = { module = "io.github.takahirom.roborazzi:roborazzi", version.ref = "roborazzi"} +roborazzi-compose = { module = "io.github.takahirom.roborazzi:roborazzi-compose", version.ref = "roborazzi"} +roborazzi-composedesktop = { module = "io.github.takahirom.roborazzi:roborazzi-compose-desktop", version.ref = "roborazzi"} +roborazzi-junit = { module = "io.github.takahirom.roborazzi:roborazzi-junit-rule", version.ref = "roborazzi"} # Build logic dependencies android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } diff --git a/haze-jetpack-compose/build.gradle.kts b/haze-jetpack-compose/build.gradle.kts index 3f4de74c..690f296f 100644 --- a/haze-jetpack-compose/build.gradle.kts +++ b/haze-jetpack-compose/build.gradle.kts @@ -8,11 +8,21 @@ plugins { id("org.jetbrains.dokka") id("com.vanniktech.maven.publish") id("me.tylerbwong.gradle.metalava") + id("io.github.takahirom.roborazzi") } android { namespace = "dev.chrisbanes.haze.jetpackcompose" + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + testOptions { + unitTests.isIncludeAndroidResources = true + } + testBuildType = "release" + buildFeatures { compose = true } @@ -24,6 +34,9 @@ android { dependencies { api(libs.androidx.compose.ui) + + testImplementation(kotlin("test")) + testImplementation(projects.internal.screenshotTest) } metalava { diff --git a/haze-jetpack-compose/screenshots/android/HazeScreenshotTest.creditCard.png b/haze-jetpack-compose/screenshots/android/HazeScreenshotTest.creditCard.png new file mode 100644 index 00000000..2135b807 --- /dev/null +++ b/haze-jetpack-compose/screenshots/android/HazeScreenshotTest.creditCard.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e56e562874c4db6ca4d05bde25b4123c6a78f298ce572c518e2e503d97f38d10 +size 886985 diff --git a/haze-jetpack-compose/screenshots/android/HazeScreenshotTest.creditCard[28].png b/haze-jetpack-compose/screenshots/android/HazeScreenshotTest.creditCard[28].png new file mode 100644 index 00000000..cf30dff1 --- /dev/null +++ b/haze-jetpack-compose/screenshots/android/HazeScreenshotTest.creditCard[28].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db7a7eacebcbb363c0a08cc16ddff3063e338819715fa208e7efc5dd60f7014d +size 882488 diff --git a/haze-jetpack-compose/screenshots/android/HazeScreenshotTest.creditCard_transparentTint.png b/haze-jetpack-compose/screenshots/android/HazeScreenshotTest.creditCard_transparentTint.png new file mode 100644 index 00000000..2135b807 --- /dev/null +++ b/haze-jetpack-compose/screenshots/android/HazeScreenshotTest.creditCard_transparentTint.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e56e562874c4db6ca4d05bde25b4123c6a78f298ce572c518e2e503d97f38d10 +size 886985 diff --git a/haze-jetpack-compose/screenshots/android/HazeScreenshotTest.creditCard_transparentTint[28].png b/haze-jetpack-compose/screenshots/android/HazeScreenshotTest.creditCard_transparentTint[28].png new file mode 100644 index 00000000..2135b807 --- /dev/null +++ b/haze-jetpack-compose/screenshots/android/HazeScreenshotTest.creditCard_transparentTint[28].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e56e562874c4db6ca4d05bde25b4123c6a78f298ce572c518e2e503d97f38d10 +size 886985 diff --git a/haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/HazeNode31.kt b/haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/HazeNode31.kt index fdfd8232..6dc9b6b6 100644 --- a/haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/HazeNode31.kt +++ b/haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/HazeNode31.kt @@ -110,9 +110,18 @@ internal class HazeNode31( if (currentValueOf(LocalInspectionMode)) { // If LocalInspectionMode is true, we're likely running in a preview/screenshot test // and therefore don't have full access to Android drawing APIs. To avoid crashing we - // no-op and return early. + // just draw the content and return early. + drawContent() return } + drawIntoCanvas { canvas -> + // Similar to above, drawRenderNode is only available on hw-accelerated canvases. + // To avoid crashing we just draw the content and return early. + if (!canvas.nativeCanvas.isHardwareAccelerated) { + drawContent() + return@draw + } + } val contentDrawScope = this diff --git a/haze-jetpack-compose/src/test/kotlin/dev/chrisbanes/haze/HazeScreenshotTest.kt b/haze-jetpack-compose/src/test/kotlin/dev/chrisbanes/haze/HazeScreenshotTest.kt new file mode 100644 index 00000000..a9909e6a --- /dev/null +++ b/haze-jetpack-compose/src/test/kotlin/dev/chrisbanes/haze/HazeScreenshotTest.kt @@ -0,0 +1,26 @@ +// Copyright 2024, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze + +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.graphics.Color +import dev.chrisbanes.haze.test.ScreenshotTest +import dev.chrisbanes.haze.test.screenshot +import kotlin.test.Test + +class HazeScreenshotTest : ScreenshotTest() { + @Test + fun creditCard() = screenshot { + MaterialTheme { + CreditCardSample() + } + } + + @Test + fun creditCard_transparentTint() = screenshot { + MaterialTheme { + CreditCardSample(tint = Color.Transparent) + } + } +} diff --git a/haze-jetpack-compose/src/test/kotlin/dev/chrisbanes/haze/ScreenshotTestContent.kt b/haze-jetpack-compose/src/test/kotlin/dev/chrisbanes/haze/ScreenshotTestContent.kt new file mode 100644 index 00000000..e8fa1dea --- /dev/null +++ b/haze-jetpack-compose/src/test/kotlin/dev/chrisbanes/haze/ScreenshotTestContent.kt @@ -0,0 +1,81 @@ +// Copyright 2024, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun CreditCardSample(tint: Color? = null) { + val hazeState = remember { HazeState() } + + Box { + // Background content + Box( + Modifier + .fillMaxSize() + .haze( + state = hazeState, + backgroundColor = Color.Blue, + tint = tint ?: Color.White.copy(alpha = 0.1f), + blurRadius = 8.dp, + ), + ) { + Spacer( + Modifier + .fillMaxSize() + .background(brush = Brush.linearGradient(colors = listOf(Color.Blue, Color.Cyan))), + ) + + Text( + text = LorumIspum, + color = LocalContentColor.current.copy(alpha = 0.2f), + modifier = Modifier.padding(24.dp), + ) + } + + // Our card + Box( + modifier = Modifier + .fillMaxWidth(0.7f) + .aspectRatio(16 / 9f) + .align(Alignment.Center) + .hazeChild(state = hazeState, shape = RoundedCornerShape(16.dp)), + ) { + Column(Modifier.padding(32.dp)) { + Text("Bank of Haze") + } + } + } +} + +val LorumIspum by lazy { + """ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sit amet congue mauris, iaculis accumsan eros. Aliquam pulvinar est ac elit vulputate egestas. Vestibulum consequat libero at sem varius, vitae semper urna rhoncus. Aliquam mollis, ipsum a convallis scelerisque, sem dui consequat leo, in tempor risus est ac mi. Nam vel tellus dolor. Nunc lobortis bibendum fermentum. Mauris sed mollis justo, eu tristique elit. Cras semper augue a tortor tempor, vitae vestibulum eros convallis. Curabitur id justo eget tortor iaculis lobortis. Integer pharetra augue ac elit porta iaculis non vitae libero. Nam eros turpis, suscipit at iaculis vitae, malesuada vel arcu. Donec tincidunt porttitor iaculis. Pellentesque non augue magna. Mauris mattis purus vitae mi maximus, id molestie ipsum facilisis. Donec bibendum gravida dolor nec suscipit. Pellentesque tempus felis iaculis, porta diam sed, tristique tortor. + +Sed vel tellus vel augue pulvinar semper sit amet eu est. In porta arcu eu sapien luctus scelerisque. In hac habitasse platea dictumst. Aenean varius lobortis malesuada. Sed vitae ornare arcu. Nunc maximus lectus purus, vel aliquet velit facilisis a. Nulla maximus bibendum magna id vulputate. Mauris volutpat lorem et risus porta dignissim. In at elit a est vulputate tincidunt. + +Nulla facilisi. Curabitur gravida quam nec massa tempus, sed placerat nunc hendrerit. Duis sit amet cursus ipsum. Phasellus eget congue lacus. Duis vehicula venenatis posuere. Morbi non tempor risus. Aenean bibendum efficitur tortor, eu interdum velit gravida rutrum. Sed tempus elementum libero. Suspendisse dapibus lorem vitae justo congue pellentesque. Phasellus et tellus sagittis, blandit nibh a, porta felis. Proin ornare eget odio eget laoreet. Cras id augue fringilla, molestie ligula sit amet, sollicitudin neque. + +Suspendisse vitae bibendum justo, nec egestas mauris. Mauris id metus mi. Morbi ut maximus ex, eu consequat elit. Sed malesuada pellentesque mauris vel molestie. Nulla facilisi. Cras pellentesque metus id nibh sodales gravida. Vivamus a feugiat felis. Vivamus et justo libero. Maecenas ac augue viverra, blandit diam sed, porttitor sapien. Proin eu eros mollis, commodo lectus nec, imperdiet nisi. Proin nulla nulla, vehicula a faucibus sit amet, auctor sed lorem. Mauris ut ipsum sit amet massa posuere maximus eget porttitor nisl. Quisque nunc dolor, pharetra id nunc sit amet, maximus convallis nunc. + +Ut magna diam, ullamcorper vel imperdiet at, dignissim sit amet turpis. Duis ut enim eu sapien fringilla placerat. Integer at dui eget leo tincidunt iaculis. Fusce nec elementum turpis. Aenean gravida, ipsum sit amet varius hendrerit, elit nisi hendrerit ex, et porta enim lorem eget mi. Duis convallis dolor a lacinia aliquam. Aliquam erat volutpat. +""".trim() +} diff --git a/haze/build.gradle.kts b/haze/build.gradle.kts index 4ba048a4..a214c3c1 100644 --- a/haze/build.gradle.kts +++ b/haze/build.gradle.kts @@ -9,10 +9,18 @@ plugins { id("org.jetbrains.dokka") id("com.vanniktech.maven.publish") id("me.tylerbwong.gradle.metalava") + id("io.github.takahirom.roborazzi") } android { namespace = "dev.chrisbanes.haze" + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + testOptions { + unitTests.isIncludeAndroidResources = true + } + testBuildType = "release" } kotlin { @@ -38,6 +46,19 @@ kotlin { val jvmMain by getting { dependsOn(skikoMain) } + + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + implementation(projects.internal.screenshotTest) + } + } + } +} + +tasks.withType().configureEach { + kotlinOptions { + freeCompilerArgs += "-Xcontext-receivers" } } diff --git a/haze/screenshots/android/HazeScreenshotTest.creditCard.png b/haze/screenshots/android/HazeScreenshotTest.creditCard.png new file mode 100644 index 00000000..cf30dff1 --- /dev/null +++ b/haze/screenshots/android/HazeScreenshotTest.creditCard.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db7a7eacebcbb363c0a08cc16ddff3063e338819715fa208e7efc5dd60f7014d +size 882488 diff --git a/haze/screenshots/android/HazeScreenshotTest.creditCard[28].png b/haze/screenshots/android/HazeScreenshotTest.creditCard[28].png new file mode 100644 index 00000000..cf30dff1 --- /dev/null +++ b/haze/screenshots/android/HazeScreenshotTest.creditCard[28].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db7a7eacebcbb363c0a08cc16ddff3063e338819715fa208e7efc5dd60f7014d +size 882488 diff --git a/haze/screenshots/android/HazeScreenshotTest.creditCard_transparentTint.png b/haze/screenshots/android/HazeScreenshotTest.creditCard_transparentTint.png new file mode 100644 index 00000000..2135b807 --- /dev/null +++ b/haze/screenshots/android/HazeScreenshotTest.creditCard_transparentTint.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e56e562874c4db6ca4d05bde25b4123c6a78f298ce572c518e2e503d97f38d10 +size 886985 diff --git a/haze/screenshots/android/HazeScreenshotTest.creditCard_transparentTint[28].png b/haze/screenshots/android/HazeScreenshotTest.creditCard_transparentTint[28].png new file mode 100644 index 00000000..2135b807 --- /dev/null +++ b/haze/screenshots/android/HazeScreenshotTest.creditCard_transparentTint[28].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e56e562874c4db6ca4d05bde25b4123c6a78f298ce572c518e2e503d97f38d10 +size 886985 diff --git a/haze/screenshots/desktop/HazeScreenshotTest.creditCard.png b/haze/screenshots/desktop/HazeScreenshotTest.creditCard.png new file mode 100644 index 00000000..3aeb923d --- /dev/null +++ b/haze/screenshots/desktop/HazeScreenshotTest.creditCard.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a2e37d26f0763257543e98528997690041f065be3043f8ffd6dc9d338434a95 +size 544567 diff --git a/haze/screenshots/desktop/HazeScreenshotTest.creditCard_transparentTint.png b/haze/screenshots/desktop/HazeScreenshotTest.creditCard_transparentTint.png new file mode 100644 index 00000000..39e70257 --- /dev/null +++ b/haze/screenshots/desktop/HazeScreenshotTest.creditCard_transparentTint.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:395ee08e35b41f2cba54d7b8279922f65cc8fbf03bc9538739259c50aaafa111 +size 550631 diff --git a/haze/src/commonTest/kotlin/dev/chrisbanes/haze/HazeScreenshotTest.kt b/haze/src/commonTest/kotlin/dev/chrisbanes/haze/HazeScreenshotTest.kt new file mode 100644 index 00000000..a9909e6a --- /dev/null +++ b/haze/src/commonTest/kotlin/dev/chrisbanes/haze/HazeScreenshotTest.kt @@ -0,0 +1,26 @@ +// Copyright 2024, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze + +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.graphics.Color +import dev.chrisbanes.haze.test.ScreenshotTest +import dev.chrisbanes.haze.test.screenshot +import kotlin.test.Test + +class HazeScreenshotTest : ScreenshotTest() { + @Test + fun creditCard() = screenshot { + MaterialTheme { + CreditCardSample() + } + } + + @Test + fun creditCard_transparentTint() = screenshot { + MaterialTheme { + CreditCardSample(tint = Color.Transparent) + } + } +} diff --git a/haze/src/commonTest/kotlin/dev/chrisbanes/haze/ScreenshotTestContent.kt b/haze/src/commonTest/kotlin/dev/chrisbanes/haze/ScreenshotTestContent.kt new file mode 100644 index 00000000..e8fa1dea --- /dev/null +++ b/haze/src/commonTest/kotlin/dev/chrisbanes/haze/ScreenshotTestContent.kt @@ -0,0 +1,81 @@ +// Copyright 2024, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun CreditCardSample(tint: Color? = null) { + val hazeState = remember { HazeState() } + + Box { + // Background content + Box( + Modifier + .fillMaxSize() + .haze( + state = hazeState, + backgroundColor = Color.Blue, + tint = tint ?: Color.White.copy(alpha = 0.1f), + blurRadius = 8.dp, + ), + ) { + Spacer( + Modifier + .fillMaxSize() + .background(brush = Brush.linearGradient(colors = listOf(Color.Blue, Color.Cyan))), + ) + + Text( + text = LorumIspum, + color = LocalContentColor.current.copy(alpha = 0.2f), + modifier = Modifier.padding(24.dp), + ) + } + + // Our card + Box( + modifier = Modifier + .fillMaxWidth(0.7f) + .aspectRatio(16 / 9f) + .align(Alignment.Center) + .hazeChild(state = hazeState, shape = RoundedCornerShape(16.dp)), + ) { + Column(Modifier.padding(32.dp)) { + Text("Bank of Haze") + } + } + } +} + +val LorumIspum by lazy { + """ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sit amet congue mauris, iaculis accumsan eros. Aliquam pulvinar est ac elit vulputate egestas. Vestibulum consequat libero at sem varius, vitae semper urna rhoncus. Aliquam mollis, ipsum a convallis scelerisque, sem dui consequat leo, in tempor risus est ac mi. Nam vel tellus dolor. Nunc lobortis bibendum fermentum. Mauris sed mollis justo, eu tristique elit. Cras semper augue a tortor tempor, vitae vestibulum eros convallis. Curabitur id justo eget tortor iaculis lobortis. Integer pharetra augue ac elit porta iaculis non vitae libero. Nam eros turpis, suscipit at iaculis vitae, malesuada vel arcu. Donec tincidunt porttitor iaculis. Pellentesque non augue magna. Mauris mattis purus vitae mi maximus, id molestie ipsum facilisis. Donec bibendum gravida dolor nec suscipit. Pellentesque tempus felis iaculis, porta diam sed, tristique tortor. + +Sed vel tellus vel augue pulvinar semper sit amet eu est. In porta arcu eu sapien luctus scelerisque. In hac habitasse platea dictumst. Aenean varius lobortis malesuada. Sed vitae ornare arcu. Nunc maximus lectus purus, vel aliquet velit facilisis a. Nulla maximus bibendum magna id vulputate. Mauris volutpat lorem et risus porta dignissim. In at elit a est vulputate tincidunt. + +Nulla facilisi. Curabitur gravida quam nec massa tempus, sed placerat nunc hendrerit. Duis sit amet cursus ipsum. Phasellus eget congue lacus. Duis vehicula venenatis posuere. Morbi non tempor risus. Aenean bibendum efficitur tortor, eu interdum velit gravida rutrum. Sed tempus elementum libero. Suspendisse dapibus lorem vitae justo congue pellentesque. Phasellus et tellus sagittis, blandit nibh a, porta felis. Proin ornare eget odio eget laoreet. Cras id augue fringilla, molestie ligula sit amet, sollicitudin neque. + +Suspendisse vitae bibendum justo, nec egestas mauris. Mauris id metus mi. Morbi ut maximus ex, eu consequat elit. Sed malesuada pellentesque mauris vel molestie. Nulla facilisi. Cras pellentesque metus id nibh sodales gravida. Vivamus a feugiat felis. Vivamus et justo libero. Maecenas ac augue viverra, blandit diam sed, porttitor sapien. Proin eu eros mollis, commodo lectus nec, imperdiet nisi. Proin nulla nulla, vehicula a faucibus sit amet, auctor sed lorem. Mauris ut ipsum sit amet massa posuere maximus eget porttitor nisl. Quisque nunc dolor, pharetra id nunc sit amet, maximus convallis nunc. + +Ut magna diam, ullamcorper vel imperdiet at, dignissim sit amet turpis. Duis ut enim eu sapien fringilla placerat. Integer at dui eget leo tincidunt iaculis. Fusce nec elementum turpis. Aenean gravida, ipsum sit amet varius hendrerit, elit nisi hendrerit ex, et porta enim lorem eget mi. Duis convallis dolor a lacinia aliquam. Aliquam erat volutpat. +""".trim() +} diff --git a/internal/screenshot-test/build.gradle.kts b/internal/screenshot-test/build.gradle.kts new file mode 100644 index 00000000..8a57b270 --- /dev/null +++ b/internal/screenshot-test/build.gradle.kts @@ -0,0 +1,73 @@ +// Copyright 2023, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + + +plugins { + id("dev.chrisbanes.android.library") + id("dev.chrisbanes.kotlin.multiplatform") + id("dev.chrisbanes.compose") + id("io.github.takahirom.roborazzi") +} + +android { + namespace = "dev.chrisbanes.haze.internal.screenshot" +} + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + api(compose.foundation) + api(compose.material3) + } + } + + val commonJvmMain by creating { + dependsOn(commonMain) + + dependencies { + api(libs.roborazzi.core) + } + } + + val androidMain by getting { + dependsOn(commonJvmMain) + + dependencies { + implementation(libs.androidx.test.ext.junit) + implementation(libs.androidx.compose.ui.test.manifest) + + @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) + implementation(compose.uiTestJUnit4) + + implementation(libs.robolectric) + + implementation(libs.roborazzi.compose) + implementation(libs.roborazzi.junit) + } + } + + val jvmMain by getting { + dependsOn(commonJvmMain) + + dependencies { + implementation(compose.desktop.currentOs) + implementation(libs.roborazzi.composedesktop) + } + } + } + + targets.configureEach { + compilations.configureEach { + compilerOptions.configure { + freeCompilerArgs.add("-Xexpect-actual-classes") + } + } + } +} + +tasks.withType().configureEach { + kotlinOptions { + freeCompilerArgs += "-Xcontext-receivers" + } +} diff --git a/internal/screenshot-test/src/androidMain/kotlin/dev/chrisbanes/haze/test/ScreenshotTest.kt b/internal/screenshot-test/src/androidMain/kotlin/dev/chrisbanes/haze/test/ScreenshotTest.kt new file mode 100644 index 00000000..22b47369 --- /dev/null +++ b/internal/screenshot-test/src/androidMain/kotlin/dev/chrisbanes/haze/test/ScreenshotTest.kt @@ -0,0 +1,41 @@ +// Copyright 2024, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.test + +import androidx.activity.ComponentActivity +import androidx.compose.runtime.Composable +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onRoot +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers +import com.github.takahirom.roborazzi.RoborazziRule +import com.github.takahirom.roborazzi.captureRoboImage +import org.junit.Rule +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode + +@RunWith(AndroidJUnit4::class) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@Config(sdk = [28, 33], qualifiers = RobolectricDeviceQualifiers.Pixel5) +actual abstract class ScreenshotTest { + @get:Rule + val composeTestRule = createAndroidComposeRule() + + @get:Rule + val roborazziRule = RoborazziRule( + composeRule = composeTestRule, + captureRoot = composeTestRule.onRoot(), + options = RoborazziRule.Options( + outputDirectoryPath = "screenshots/${HazeRoborazziDefaults.outputDirectoryName}", + roborazziOptions = HazeRoborazziDefaults.roborazziOptions, + ), + ) +} + +actual val HazeRoborazziDefaults.outputDirectoryName: String get() = "android" + +actual fun ScreenshotTest.screenshot(content: @Composable () -> Unit) { + captureRoboImage(content = content) +} diff --git a/internal/screenshot-test/src/commonJvmMain/kotlin/dev/chrisbanes/haze/test/HazeRoborazziDefaults.kt b/internal/screenshot-test/src/commonJvmMain/kotlin/dev/chrisbanes/haze/test/HazeRoborazziDefaults.kt new file mode 100644 index 00000000..02b1e3dd --- /dev/null +++ b/internal/screenshot-test/src/commonJvmMain/kotlin/dev/chrisbanes/haze/test/HazeRoborazziDefaults.kt @@ -0,0 +1,20 @@ +// Copyright 2024, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.test + +import com.dropbox.differ.SimpleImageComparator +import com.github.takahirom.roborazzi.ExperimentalRoborazziApi +import com.github.takahirom.roborazzi.RoborazziOptions + +@OptIn(ExperimentalRoborazziApi::class) +object HazeRoborazziDefaults { + val roborazziOptions = RoborazziOptions( + compareOptions = RoborazziOptions.CompareOptions( + changeThreshold = 0.01f, + imageComparator = SimpleImageComparator(hShift = 1, vShift = 1), + ), + ) +} + +expect val HazeRoborazziDefaults.outputDirectoryName: String diff --git a/internal/screenshot-test/src/commonMain/kotlin/dev/chrisbanes/haze/test/ScreenshotTest.kt b/internal/screenshot-test/src/commonMain/kotlin/dev/chrisbanes/haze/test/ScreenshotTest.kt new file mode 100644 index 00000000..9974a3f9 --- /dev/null +++ b/internal/screenshot-test/src/commonMain/kotlin/dev/chrisbanes/haze/test/ScreenshotTest.kt @@ -0,0 +1,10 @@ +// Copyright 2024, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.test + +import androidx.compose.runtime.Composable + +expect abstract class ScreenshotTest() + +expect fun ScreenshotTest.screenshot(content: @Composable () -> Unit) diff --git a/internal/screenshot-test/src/iosMain/kotlin/dev/chrisbanes/haze/test/ScreenshotTest.kt b/internal/screenshot-test/src/iosMain/kotlin/dev/chrisbanes/haze/test/ScreenshotTest.kt new file mode 100644 index 00000000..c21007a9 --- /dev/null +++ b/internal/screenshot-test/src/iosMain/kotlin/dev/chrisbanes/haze/test/ScreenshotTest.kt @@ -0,0 +1,12 @@ +// Copyright 2024, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.test + +import androidx.compose.runtime.Composable + +actual abstract class ScreenshotTest + +actual fun ScreenshotTest.screenshot(content: @Composable () -> Unit) { + // no-op on iOS +} diff --git a/internal/screenshot-test/src/jvmMain/kotlin/dev/chrisbanes/haze/test/ScreenshotTest.kt b/internal/screenshot-test/src/jvmMain/kotlin/dev/chrisbanes/haze/test/ScreenshotTest.kt new file mode 100644 index 00000000..e01529a5 --- /dev/null +++ b/internal/screenshot-test/src/jvmMain/kotlin/dev/chrisbanes/haze/test/ScreenshotTest.kt @@ -0,0 +1,50 @@ +// Copyright 2024, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.test + +import androidx.compose.runtime.Composable +import androidx.compose.ui.test.DesktopComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.unit.Density +import com.github.takahirom.roborazzi.ExperimentalRoborazziApi +import com.github.takahirom.roborazzi.InternalRoborazziApi +import com.github.takahirom.roborazzi.RoborazziOptions +import com.github.takahirom.roborazzi.provideRoborazziContext +import io.github.takahirom.roborazzi.captureRoboImage +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +actual abstract class ScreenshotTest + +actual val HazeRoborazziDefaults.outputDirectoryName: String get() = "desktop" + +actual fun ScreenshotTest.screenshot(content: @Composable () -> Unit) { + @OptIn(ExperimentalTestApi::class) + captureRoborazziImage(content = content) +} + +@OptIn(ExperimentalRoborazziApi::class, InternalRoborazziApi::class) +@ExperimentalTestApi +private fun captureRoborazziImage( + width: Int = 1080, + height: Int = 1920, + density: Density = Density(2.75f), + roborazziOptions: RoborazziOptions = HazeRoborazziDefaults.roborazziOptions, + effectContext: CoroutineContext = EmptyCoroutineContext, + content: @Composable () -> Unit, +) { + DesktopComposeUiTest( + width = width, + height = height, + effectContext = effectContext, + density = density, + ).runTest { + provideRoborazziContext().apply { + setRuleOverrideOutputDirectory("screenshots/desktop") + } + setContent(content) + onRoot().captureRoboImage(roborazziOptions = roborazziOptions) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index de945447..3be2a33f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -44,6 +44,7 @@ rootProject.name = "haze-root" include( ":haze", ":haze-jetpack-compose", + ":internal:screenshot-test", ":sample:shared", ":sample:android", ":sample:android-jetpack",