Skip to content

Commit 62fa309

Browse files
committed
「Composable Preview Scannerを使ってプレビュー画面のスクリーンショットを撮る」追加
1 parent f817d46 commit 62fa309

File tree

17 files changed

+579
-32
lines changed

17 files changed

+579
-32
lines changed

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ Now in Android Appで使用されている技術は次のとおりで、これ
4444
- Composeのユニットテストについて学ぶ
4545
- ViewModelを結合してComposeをテストする
4646
- ComposeのNavigationをテストする
47+
- Jetpack Composeの画面スクリーンショットを使ってVisual Regression Testを実現する
48+
- Composeのプレビュー画面でVisual Regression Testを行う
49+
- Visual Regression TestをCIで実行する
50+
- 様々なケースでComposeの画面スクリーンショットを撮る
51+
- Composable Preview Scannerを使ってプレビュー画面のスクリーンショットを撮る
4752

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

app/build.gradle.kts

+22-6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17+
import com.github.takahirom.roborazzi.ExperimentalRoborazziApi
1718
import com.google.samples.apps.nowinandroid.NiaBuildType
1819

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

3637
// Custom test runner to set up Hilt dependency graph
37-
testInstrumentationRunner = "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner"
38+
testInstrumentationRunner =
39+
"com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner"
3840
vectorDrawables {
3941
useSupportLibrary = true
4042
}
@@ -47,7 +49,10 @@ android {
4749
val release by getting {
4850
isMinifyEnabled = true
4951
applicationIdSuffix = NiaBuildType.RELEASE.applicationIdSuffix
50-
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
52+
proguardFiles(
53+
getDefaultProguardFile("proguard-android-optimize.txt"),
54+
"proguard-rules.pro"
55+
)
5156

5257
// To publish on the Play store a private signing key is required, but to allow anyone
5358
// who clones the code to sign and run the release variant, use the debug signing key.
@@ -77,9 +82,9 @@ android {
7782
testOptions {
7883
unitTests {
7984
all {
80-
it.systemProperty(
81-
"roborazzi.output.dir",
82-
rootProject.file("screenshots").absolutePath
85+
it.systemProperties(
86+
"roborazzi.output.dir" to rootProject.file("screenshots").absolutePath,
87+
"robolectric.pixelCopyRenderMode" to "hardware"
8388
)
8489
}
8590
isIncludeAndroidResources = true
@@ -153,6 +158,8 @@ dependencies {
153158

154159
testImplementation(libs.roborazzi)
155160
testImplementation(libs.roborazzi.compose)
161+
testImplementation(libs.roborazzi.compose.preview.scanner.support)
162+
testImplementation(libs.compose.preview.scanner)
156163
testImplementation(libs.robolectric)
157164
}
158165

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

168175
roborazzi {
169176
outputDir.set(rootProject.file("screenshots"))
170-
}
177+
@OptIn(ExperimentalRoborazziApi::class)
178+
generateComposePreviewRobolectricTests {
179+
enable = true
180+
testerQualifiedClassName = "com.google.samples.apps.nowinandroid.MyComposePreviewTester"
181+
packages = listOf(
182+
"com.google.samples.apps.nowinandroid.feature.interests",
183+
"com.google.samples.apps.nowinandroid.feature.foryou"
184+
)
185+
}
186+
}

app/src/testAnswer/java/com/github/takahirom/roborazzi/.gitkeep

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package com.google.samples.apps.nowinandroid
2+
3+
import android.content.Context
4+
import android.content.res.Configuration
5+
import androidx.activity.ComponentActivity
6+
import androidx.compose.runtime.Composable
7+
import androidx.compose.runtime.CompositionLocalProvider
8+
import androidx.compose.ui.platform.LocalDensity
9+
import androidx.compose.ui.test.junit4.createAndroidComposeRule
10+
import androidx.compose.ui.test.onRoot
11+
import androidx.compose.ui.unit.Density
12+
import androidx.test.core.app.ApplicationProvider
13+
import com.github.takahirom.roborazzi.ComposePreviewTester
14+
import com.github.takahirom.roborazzi.ExperimentalRoborazziApi
15+
import com.github.takahirom.roborazzi.captureRoboImage
16+
import com.google.samples.apps.nowinandroid.core.ui.DelayedPreview
17+
import org.robolectric.RuntimeEnvironment
18+
import org.robolectric.Shadows
19+
import org.robolectric.shadows.ShadowDisplay
20+
import sergio.sastre.composable.preview.scanner.android.AndroidComposablePreviewScanner
21+
import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo
22+
import sergio.sastre.composable.preview.scanner.android.screenshotid.AndroidPreviewScreenshotIdBuilder
23+
import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview
24+
import sergio.sastre.composable.preview.scanner.core.preview.getAnnotation
25+
import kotlin.math.roundToInt
26+
27+
@OptIn(ExperimentalRoborazziApi::class)
28+
class MyComposePreviewTester : ComposePreviewTester<AndroidPreviewInfo> {
29+
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
30+
override fun options(): ComposePreviewTester.Options {
31+
val testLifecycleOptions = ComposePreviewTester.Options.JUnit4TestLifecycleOptions(
32+
testRuleFactory = { composeTestRule }
33+
)
34+
return super.options().copy(testLifecycleOptions = testLifecycleOptions)
35+
}
36+
37+
override fun previews(): List<ComposablePreview<AndroidPreviewInfo>> {
38+
val options = options()
39+
return AndroidComposablePreviewScanner()
40+
.scanPackageTrees(*options.scanOptions.packages.toTypedArray())
41+
.includeAnnotationInfoForAllOf(DelayedPreview::class.java)
42+
.getPreviews()
43+
}
44+
45+
override fun test(preview: ComposablePreview<AndroidPreviewInfo>) {
46+
val delay = preview.getAnnotation<DelayedPreview>()?.delay ?: 0L
47+
val previewScannerFileName =
48+
AndroidPreviewScreenshotIdBuilder(preview).build()
49+
val fileName =
50+
if (delay == 0L) previewScannerFileName else "${previewScannerFileName}_delay$delay"
51+
val filePath = "$fileName.png"
52+
preview.myApplyToRobolectricConfiguration()
53+
composeTestRule.activityRule.scenario.recreate()
54+
composeTestRule.apply {
55+
try {
56+
if (delay != 0L) {
57+
mainClock.autoAdvance = false
58+
}
59+
setContent {
60+
ApplyToCompositionLocal(preview) {
61+
preview()
62+
}
63+
}
64+
if (delay != 0L) {
65+
mainClock.advanceTimeBy(delay)
66+
}
67+
onRoot().captureRoboImage(filePath = filePath)
68+
} finally {
69+
mainClock.autoAdvance = true
70+
}
71+
}
72+
}
73+
}
74+
75+
@Composable
76+
fun ApplyToCompositionLocal(
77+
preview: ComposablePreview<AndroidPreviewInfo>,
78+
content: @Composable () -> Unit
79+
) {
80+
val fontScale = preview.previewInfo.fontScale
81+
val density = LocalDensity.current
82+
val customDensity =
83+
Density(density = density.density, fontScale = density.fontScale * fontScale)
84+
CompositionLocalProvider(LocalDensity provides customDensity) {
85+
content()
86+
}
87+
88+
}
89+
90+
91+
fun ComposablePreview<AndroidPreviewInfo>.myApplyToRobolectricConfiguration() {
92+
val preview = this
93+
// ナイトモード
94+
when (preview.previewInfo.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
95+
Configuration.UI_MODE_NIGHT_YES -> RuntimeEnvironment.setQualifiers("+night")
96+
Configuration.UI_MODE_NIGHT_NO -> RuntimeEnvironment.setQualifiers("+notnight")
97+
else -> { /* do nothing */
98+
}
99+
}
100+
101+
// 画面サイズ
102+
if (preview.previewInfo.widthDp != -1 && preview.previewInfo.heightDp != -1) {
103+
setDisplaySize(preview.previewInfo.widthDp, preview.previewInfo.heightDp)
104+
}
105+
}
106+
107+
private fun setDisplaySize(widthDp: Int, heightDp: Int) {
108+
val context = ApplicationProvider.getApplicationContext<Context>()
109+
val display = ShadowDisplay.getDefaultDisplay()
110+
val density = context.resources.displayMetrics.density
111+
widthDp.let {
112+
val widthPx = (widthDp * density).roundToInt()
113+
Shadows.shadowOf(display).setWidth(widthPx)
114+
}
115+
heightDp.let {
116+
val heightPx = (heightDp * density).roundToInt()
117+
Shadows.shadowOf(display).setHeight(heightPx)
118+
}
119+
}

build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import java.io.File
2727
* Configure Compose-specific options
2828
*/
2929
internal fun Project.configureAndroidCompose(
30-
commonExtension: CommonExtension<*, *, *, *, *>,
30+
commonExtension: CommonExtension<*, *, *, *, *, *>,
3131
) {
3232
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
3333

build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
3030
* Configure base Kotlin with Android options
3131
*/
3232
internal fun Project.configureKotlinAndroid(
33-
commonExtension: CommonExtension<*, *, *, *, *>,
33+
commonExtension: CommonExtension<*, *, *, *, *, *>,
3434
) {
3535
commonExtension.apply {
3636
compileSdk = 34
@@ -71,6 +71,6 @@ internal fun Project.configureKotlinAndroid(
7171
}
7272
}
7373

74-
fun CommonExtension<*, *, *, *, *>.kotlinOptions(block: KotlinJvmOptions.() -> Unit) {
74+
fun CommonExtension<*, *, *, *, *, *>.kotlinOptions(block: KotlinJvmOptions.() -> Unit) {
7575
(this as ExtensionAware).extensions.configure("kotlinOptions", block)
7676
}

build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ enum class NiaFlavor(val dimension: FlavorDimension, val applicationIdSuffix: St
2323
}
2424

2525
fun Project.configureFlavors(
26-
commonExtension: CommonExtension<*, *, *, *, *>,
26+
commonExtension: CommonExtension<*, *, *, *, *, *>,
2727
flavorConfigurationBlock: ProductFlavor.(flavor: NiaFlavor) -> Unit = {}
2828
) {
2929
commonExtension.apply {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package com.google.samples.apps.nowinandroid.core.ui
2+
3+
annotation class DelayedPreview(val delay: Long)

docs/handson/UILayerTest.md

+1
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,4 @@ UIテストには、コード変更前後の画面スクリーンショットを
6464
- [Composeのプレビュー画面でVisual Regression Testを行う](./VisualRegressionTest_Preview.md)
6565
- [Visual Regression TestをCIで実行する](./VisualRegressionTest_CI.md)
6666
- [様々なケースでComposeの画面スクリーンショットを撮る](./VisualRegressionTest_Advanced.md)
67+
- [Composable Preview Scannerを使ってプレビュー画面のスクリーンショットを撮る](./VisualRegressionTest_Preview_ComposablePreviewScanner.md)

docs/handson/VisualRegressionTest_Advanced.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

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

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

3131
```kotlin
3232
@RunWith(ParameterizedRobolectricTestRunner::class)

docs/handson/VisualRegressionTest_Preview.md

+7-7
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ plugins {
9797
}
9898

9999
dependencies {
100-
implementation("com.airbnb.android:showkase:1.0.2")
101-
ksp("com.airbnb.android:showkase-processor:1.0.2")
100+
implementation("com.airbnb.android:showkase:1.0.3")
101+
ksp("com.airbnb.android:showkase-processor:1.0.3")
102102
}
103103
```
104104

@@ -238,7 +238,7 @@ RoborazziはRobolectricに依存しているため、RoborazziとRobolectricの
238238
239239
```groovy
240240
plugins {
241-
id("io.github.takahirom.roborazzi") version "1.10.1" apply false
241+
id("io.github.takahirom.roborazzi") version "1.26.0" apply false
242242
}
243243
```
244244
@@ -248,7 +248,7 @@ plugins {
248248
249249
```kotlin
250250
plugins {
251-
id("io.github.takahirom.roborazzi") version "1.10.1" apply false
251+
id("io.github.takahirom.roborazzi") version "1.26.0"
252252
}
253253
254254
android {
@@ -260,9 +260,9 @@ android {
260260
}
261261
262262
dependencies {
263-
testImplementation("io.github.takahirom.roborazzi:roborazzi:1.10.1")
264-
testImplementation("io.github.takahirom.roborazzi:roborazzi-compose:1.10.1")
265-
testImplementation("org.robolectric:robolectric:4.11.1")
263+
testImplementation("io.github.takahirom.roborazzi:roborazzi:1.26.0")
264+
testImplementation("io.github.takahirom.roborazzi:roborazzi-compose:1.26.0")
265+
testImplementation("org.robolectric:robolectric:4.13")
266266
}
267267
```
268268

0 commit comments

Comments
 (0)