Skip to content

Commit

Permalink
「Composable Preview Scannerを使ってプレビュー画面のスクリーンショットを撮る」追加
Browse files Browse the repository at this point in the history
  • Loading branch information
sumio committed Sep 10, 2024
1 parent f817d46 commit 62fa309
Show file tree
Hide file tree
Showing 17 changed files with 579 additions and 32 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ Now in Android Appで使用されている技術は次のとおりで、これ
- Composeのユニットテストについて学ぶ
- ViewModelを結合してComposeをテストする
- ComposeのNavigationをテストする
- Jetpack Composeの画面スクリーンショットを使ってVisual Regression Testを実現する
- Composeのプレビュー画面でVisual Regression Testを行う
- Visual Regression TestをCIで実行する
- 様々なケースでComposeの画面スクリーンショットを撮る
- Composable Preview Scannerを使ってプレビュー画面のスクリーンショットを撮る

## オリジナルのNow in Android Appからの変更点

Expand Down
28 changes: 22 additions & 6 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.github.takahirom.roborazzi.ExperimentalRoborazziApi
import com.google.samples.apps.nowinandroid.NiaBuildType

plugins {
Expand All @@ -34,7 +35,8 @@ android {
versionName = "0.0.3" // X.Y.Z; X = Major, Y = minor, Z = Patch level

// Custom test runner to set up Hilt dependency graph
testInstrumentationRunner = "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner"
testInstrumentationRunner =
"com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner"
vectorDrawables {
useSupportLibrary = true
}
Expand All @@ -47,7 +49,10 @@ android {
val release by getting {
isMinifyEnabled = true
applicationIdSuffix = NiaBuildType.RELEASE.applicationIdSuffix
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)

// To publish on the Play store a private signing key is required, but to allow anyone
// who clones the code to sign and run the release variant, use the debug signing key.
Expand Down Expand Up @@ -77,9 +82,9 @@ android {
testOptions {
unitTests {
all {
it.systemProperty(
"roborazzi.output.dir",
rootProject.file("screenshots").absolutePath
it.systemProperties(
"roborazzi.output.dir" to rootProject.file("screenshots").absolutePath,
"robolectric.pixelCopyRenderMode" to "hardware"
)
}
isIncludeAndroidResources = true
Expand Down Expand Up @@ -153,6 +158,8 @@ dependencies {

testImplementation(libs.roborazzi)
testImplementation(libs.roborazzi.compose)
testImplementation(libs.roborazzi.compose.preview.scanner.support)
testImplementation(libs.compose.preview.scanner)
testImplementation(libs.robolectric)
}

Expand All @@ -167,4 +174,13 @@ configurations.configureEach {

roborazzi {
outputDir.set(rootProject.file("screenshots"))
}
@OptIn(ExperimentalRoborazziApi::class)
generateComposePreviewRobolectricTests {
enable = true
testerQualifiedClassName = "com.google.samples.apps.nowinandroid.MyComposePreviewTester"
packages = listOf(
"com.google.samples.apps.nowinandroid.feature.interests",
"com.google.samples.apps.nowinandroid.feature.foryou"
)
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package com.google.samples.apps.nowinandroid

import android.content.Context
import android.content.res.Configuration
import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.unit.Density
import androidx.test.core.app.ApplicationProvider
import com.github.takahirom.roborazzi.ComposePreviewTester
import com.github.takahirom.roborazzi.ExperimentalRoborazziApi
import com.github.takahirom.roborazzi.captureRoboImage
import com.google.samples.apps.nowinandroid.core.ui.DelayedPreview
import org.robolectric.RuntimeEnvironment
import org.robolectric.Shadows
import org.robolectric.shadows.ShadowDisplay
import sergio.sastre.composable.preview.scanner.android.AndroidComposablePreviewScanner
import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo
import sergio.sastre.composable.preview.scanner.android.screenshotid.AndroidPreviewScreenshotIdBuilder
import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview
import sergio.sastre.composable.preview.scanner.core.preview.getAnnotation
import kotlin.math.roundToInt

@OptIn(ExperimentalRoborazziApi::class)
class MyComposePreviewTester : ComposePreviewTester<AndroidPreviewInfo> {
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
override fun options(): ComposePreviewTester.Options {
val testLifecycleOptions = ComposePreviewTester.Options.JUnit4TestLifecycleOptions(
testRuleFactory = { composeTestRule }
)
return super.options().copy(testLifecycleOptions = testLifecycleOptions)
}

override fun previews(): List<ComposablePreview<AndroidPreviewInfo>> {
val options = options()
return AndroidComposablePreviewScanner()
.scanPackageTrees(*options.scanOptions.packages.toTypedArray())
.includeAnnotationInfoForAllOf(DelayedPreview::class.java)
.getPreviews()
}

override fun test(preview: ComposablePreview<AndroidPreviewInfo>) {
val delay = preview.getAnnotation<DelayedPreview>()?.delay ?: 0L
val previewScannerFileName =
AndroidPreviewScreenshotIdBuilder(preview).build()
val fileName =
if (delay == 0L) previewScannerFileName else "${previewScannerFileName}_delay$delay"
val filePath = "$fileName.png"
preview.myApplyToRobolectricConfiguration()
composeTestRule.activityRule.scenario.recreate()
composeTestRule.apply {
try {
if (delay != 0L) {
mainClock.autoAdvance = false
}
setContent {
ApplyToCompositionLocal(preview) {
preview()
}
}
if (delay != 0L) {
mainClock.advanceTimeBy(delay)
}
onRoot().captureRoboImage(filePath = filePath)
} finally {
mainClock.autoAdvance = true
}
}
}
}

@Composable
fun ApplyToCompositionLocal(
preview: ComposablePreview<AndroidPreviewInfo>,
content: @Composable () -> Unit
) {
val fontScale = preview.previewInfo.fontScale
val density = LocalDensity.current
val customDensity =
Density(density = density.density, fontScale = density.fontScale * fontScale)
CompositionLocalProvider(LocalDensity provides customDensity) {
content()
}

}


fun ComposablePreview<AndroidPreviewInfo>.myApplyToRobolectricConfiguration() {
val preview = this
// ナイトモード
when (preview.previewInfo.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
Configuration.UI_MODE_NIGHT_YES -> RuntimeEnvironment.setQualifiers("+night")
Configuration.UI_MODE_NIGHT_NO -> RuntimeEnvironment.setQualifiers("+notnight")
else -> { /* do nothing */
}
}

// 画面サイズ
if (preview.previewInfo.widthDp != -1 && preview.previewInfo.heightDp != -1) {
setDisplaySize(preview.previewInfo.widthDp, preview.previewInfo.heightDp)
}
}

private fun setDisplaySize(widthDp: Int, heightDp: Int) {
val context = ApplicationProvider.getApplicationContext<Context>()
val display = ShadowDisplay.getDefaultDisplay()
val density = context.resources.displayMetrics.density
widthDp.let {
val widthPx = (widthDp * density).roundToInt()
Shadows.shadowOf(display).setWidth(widthPx)
}
heightDp.let {
val heightPx = (heightDp * density).roundToInt()
Shadows.shadowOf(display).setHeight(heightPx)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import java.io.File
* Configure Compose-specific options
*/
internal fun Project.configureAndroidCompose(
commonExtension: CommonExtension<*, *, *, *, *>,
commonExtension: CommonExtension<*, *, *, *, *, *>,
) {
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
* Configure base Kotlin with Android options
*/
internal fun Project.configureKotlinAndroid(
commonExtension: CommonExtension<*, *, *, *, *>,
commonExtension: CommonExtension<*, *, *, *, *, *>,
) {
commonExtension.apply {
compileSdk = 34
Expand Down Expand Up @@ -71,6 +71,6 @@ internal fun Project.configureKotlinAndroid(
}
}

fun CommonExtension<*, *, *, *, *>.kotlinOptions(block: KotlinJvmOptions.() -> Unit) {
fun CommonExtension<*, *, *, *, *, *>.kotlinOptions(block: KotlinJvmOptions.() -> Unit) {
(this as ExtensionAware).extensions.configure("kotlinOptions", block)
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ enum class NiaFlavor(val dimension: FlavorDimension, val applicationIdSuffix: St
}

fun Project.configureFlavors(
commonExtension: CommonExtension<*, *, *, *, *>,
commonExtension: CommonExtension<*, *, *, *, *, *>,
flavorConfigurationBlock: ProductFlavor.(flavor: NiaFlavor) -> Unit = {}
) {
commonExtension.apply {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.google.samples.apps.nowinandroid.core.ui

annotation class DelayedPreview(val delay: Long)
1 change: 1 addition & 0 deletions docs/handson/UILayerTest.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,4 @@ UIテストには、コード変更前後の画面スクリーンショットを
- [Composeのプレビュー画面でVisual Regression Testを行う](./VisualRegressionTest_Preview.md)
- [Visual Regression TestをCIで実行する](./VisualRegressionTest_CI.md)
- [様々なケースでComposeの画面スクリーンショットを撮る](./VisualRegressionTest_Advanced.md)
- [Composable Preview Scannerを使ってプレビュー画面のスクリーンショットを撮る](./VisualRegressionTest_Preview_ComposablePreviewScanner.md)
2 changes: 1 addition & 1 deletion docs/handson/VisualRegressionTest_Advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

#### ユースケース1:1つの`@Preview`アノテーションにつき、複数パターンのスクリーンショットを保存する

このユースケースは、「[Composeのプレビュー画面でVisual Regression Testを行う](VisualRegressionTest_Test.md)」で紹介した`ParameterizedRobolectricTestRunner`の仕組みを使うと自然に実現できる。
このユースケースは、「[Composeのプレビュー画面でVisual Regression Testを行う](VisualRegressionTest_Preview.md)」で紹介した`ParameterizedRobolectricTestRunner`の仕組みを使うと自然に実現できる。

```kotlin
@RunWith(ParameterizedRobolectricTestRunner::class)
Expand Down
14 changes: 7 additions & 7 deletions docs/handson/VisualRegressionTest_Preview.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ plugins {
}

dependencies {
implementation("com.airbnb.android:showkase:1.0.2")
ksp("com.airbnb.android:showkase-processor:1.0.2")
implementation("com.airbnb.android:showkase:1.0.3")
ksp("com.airbnb.android:showkase-processor:1.0.3")
}
```

Expand Down Expand Up @@ -238,7 +238,7 @@ RoborazziはRobolectricに依存しているため、RoborazziとRobolectricの
```groovy
plugins {
id("io.github.takahirom.roborazzi") version "1.10.1" apply false
id("io.github.takahirom.roborazzi") version "1.26.0" apply false
}
```
Expand All @@ -248,7 +248,7 @@ plugins {
```kotlin
plugins {
id("io.github.takahirom.roborazzi") version "1.10.1" apply false
id("io.github.takahirom.roborazzi") version "1.26.0"
}
android {
Expand All @@ -260,9 +260,9 @@ android {
}
dependencies {
testImplementation("io.github.takahirom.roborazzi:roborazzi:1.10.1")
testImplementation("io.github.takahirom.roborazzi:roborazzi-compose:1.10.1")
testImplementation("org.robolectric:robolectric:4.11.1")
testImplementation("io.github.takahirom.roborazzi:roborazzi:1.26.0")
testImplementation("io.github.takahirom.roborazzi:roborazzi-compose:1.26.0")
testImplementation("org.robolectric:robolectric:4.13")
}
```
Expand Down
Loading

0 comments on commit 62fa309

Please sign in to comment.