Skip to content

Commit

Permalink
Merge pull request #6 from DeNA/visual_regression_test_advanced
Browse files Browse the repository at this point in the history
ハンズオン 「さまざまなケースでComposeの画面スクリーンショットを撮る」を追加
  • Loading branch information
sumio authored May 14, 2024
2 parents b036476 + 1945f93 commit f817d46
Show file tree
Hide file tree
Showing 25 changed files with 1,531 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.consumedWindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
Expand Down Expand Up @@ -140,7 +140,7 @@ fun NiaApp(
Modifier
.fillMaxSize()
.padding(padding)
.consumedWindowInsets(padding)
.consumeWindowInsets(padding)
.windowInsetsPadding(
WindowInsets.safeDrawing.only(
WindowInsetsSides.Horizontal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,10 @@ class AllPreviewScreenshotTest(
override fun toString() = showkaseBrowserComponent.componentKey
}


@ParameterizedRobolectricTestRunner.Parameters(name = "[{index}] {0}")
@JvmStatic
fun components(): Iterable<Array<*>> = Showkase.getMetadata().componentList.map {
arrayOf(TestCase(it))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package com.google.samples.apps.nowinandroid

import android.content.Context
import androidx.activity.ComponentActivity
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.filter
import androidx.compose.ui.test.isRoot
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.unit.Density
import androidx.test.core.app.ApplicationProvider
import com.airbnb.android.showkase.models.Showkase
import com.airbnb.android.showkase.models.ShowkaseBrowserComponent
import com.github.takahirom.roborazzi.ExperimentalRoborazziApi
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
import com.github.takahirom.roborazzi.captureRoboImage
import com.github.takahirom.roborazzi.captureScreenRoboImage
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.ParameterizedRobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.Shadows
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
import org.robolectric.shadows.ShadowDisplay
import kotlin.math.roundToInt

@RunWith(ParameterizedRobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(qualifiers = RobolectricDeviceQualifiers.Pixel7)
class VariousPreviewScreenshotTest(
private val testCase: TestCase
) {
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
lateinit var context: Context

@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
}

@Test
fun testDefault() {
val filePath = "${testCase.showkaseBrowserComponent.componentKey}.png"
capturePreview(filePath = filePath)
}

@Test
fun testFont2x() {
val filePath = "${testCase.showkaseBrowserComponent.componentKey}_font2x.png"
capturePreview(filePath = filePath, fontMagnification = 2f)
}

@OptIn(ExperimentalRoborazziApi::class)
private fun capturePreview(filePath: String, fontMagnification: Float = 1f) {
setGroupParameterToQualifiers(group = testCase.showkaseBrowserComponent.group)
val widthDp = testCase.showkaseBrowserComponent.widthDp
val heightDp = testCase.showkaseBrowserComponent.heightDp
if (widthDp != null || heightDp != null) {
setDisplaySize(widthDp, heightDp)
}
// Activityを再生成して変更した画面サイズを反映させる
composeTestRule.activityRule.scenario.recreate()

composeTestRule.setContent {
val density = LocalDensity.current
val customDensity = Density(
fontScale = density.fontScale * fontMagnification,
density = density.density,
)
CompositionLocalProvider(
LocalInspectionMode provides true,
LocalDensity provides customDensity,
) {
testCase.showkaseBrowserComponent.component()
}
}

kotlin.runCatching {
// 複数Windowがある場合、子コンポーネントがいる最初のものをとってくる
composeTestRule.onAllNodes(isRoot())
.filter(hasAnyChild())
.onFirst()
.assertExists() // 念のため存在していることをassertしている。assertに失敗したらnullを返し、captureScreenRoboImage()を使って全画面スクリーンショットを撮る
}.getOrNull()?.captureRoboImage(filePath) ?: captureScreenRoboImage(filePath)
}

private fun setGroupParameterToQualifiers(group: String) {
if (group != "Default Group") {
val tags = testCase.showkaseBrowserComponent.group.split("-")
tags.forEach { tag ->
RuntimeEnvironment.setQualifiers("+$tag")
}
}
}

private fun setDisplaySize(widthDp: Int?, heightDp: Int?) {
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)
}
}

fun hasAnyChild(): SemanticsMatcher {
return SemanticsMatcher("hasAnyChildThat") {
it.children.isNotEmpty()
}
}

companion object {
class TestCase(
val showkaseBrowserComponent: ShowkaseBrowserComponent
) {
override fun toString() = showkaseBrowserComponent.componentKey
}


@ParameterizedRobolectricTestRunner.Parameters(name = "[{index}] {0}")
@JvmStatic
fun components(): Iterable<Array<*>> = Showkase.getMetadata().componentList.map {
arrayOf(TestCase(it))
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,40 @@
package com.google.samples.apps.nowinandroid

class AllPreviewScreenshotTest()
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onRoot
import com.airbnb.android.showkase.models.Showkase
import com.airbnb.android.showkase.models.ShowkaseBrowserComponent
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
import com.github.takahirom.roborazzi.captureRoboImage
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.ParameterizedRobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode

@RunWith(ParameterizedRobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(qualifiers = RobolectricDeviceQualifiers.Pixel7)
class AllPreviewScreenshotTest(
private val showkaseBrowserComponent: ShowkaseBrowserComponent,
) {
@get:Rule
val composeTestRule = createComposeRule()

@Test
fun test() {
composeTestRule.setContent {
// showkaseBrowserComponent.component() がPreview指定されているComposable関数
showkaseBrowserComponent.component()
}
// ここでスクリーンショットを取得する
composeTestRule.onRoot().captureRoboImage()
}

companion object {
@ParameterizedRobolectricTestRunner.Parameters
@JvmStatic
fun components(): Iterable<Array<*>> = Showkase.getMetadata().componentList.map { arrayOf(it) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.google.samples.apps.nowinandroid

import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.filter
import androidx.compose.ui.test.isRoot
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onFirst
import com.airbnb.android.showkase.models.Showkase
import com.airbnb.android.showkase.models.ShowkaseBrowserComponent
import com.github.takahirom.roborazzi.ExperimentalRoborazziApi
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
import com.github.takahirom.roborazzi.captureRoboImage
import com.github.takahirom.roborazzi.captureScreenRoboImage
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.ParameterizedRobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode

@RunWith(ParameterizedRobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(qualifiers = RobolectricDeviceQualifiers.Pixel7)
class VariousPreviewScreenshotTest(
private val testCase: TestCase
) {
@get:Rule
val composeTestRule = createComposeRule()

@OptIn(ExperimentalRoborazziApi::class)
@Test
fun test() {
val filePath = "${testCase.showkaseBrowserComponent.componentKey}.png"
composeTestRule.setContent {
CompositionLocalProvider(
LocalInspectionMode provides true,
) {
testCase.showkaseBrowserComponent.component()
}
}
kotlin.runCatching {
// 複数Windowがある場合、子コンポーネントがいる最初のものをとってくる
composeTestRule.onAllNodes(isRoot())
.filter(hasAnyChild())
.onFirst()
.assertExists() // 念のため存在していることをassertしている。assertに失敗したらnullを返し、captureScreenRoboImage()を使って全画面スクリーンショットを撮る
}.getOrNull()?.captureRoboImage(filePath) ?: captureScreenRoboImage(filePath)
}

fun hasAnyChild(): SemanticsMatcher {
return SemanticsMatcher("hasAnyChildThat") {
it.children.isNotEmpty()
}
}

companion object {
class TestCase(
val showkaseBrowserComponent: ShowkaseBrowserComponent
) {
override fun toString() = showkaseBrowserComponent.componentKey
}

@ParameterizedRobolectricTestRunner.Parameters(name = "[{index}] {0}")
@JvmStatic
fun components(): Iterable<Array<*>> = Showkase.getMetadata().componentList.map {
arrayOf(TestCase(it))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,15 @@ import androidx.compose.ui.tooling.preview.Preview
* Multipreview annotation that represents various device sizes. Add this annotation to a composable
* to render various devices.
*/
@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480")
@Preview(name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480")
@Preview(name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480")
@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480")
annotation class DevicePreviews
//@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480")
//@Preview(group = "LandscapePreview", name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480")
//@Preview(group = "FoldablePreview", name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480")
//@Preview(group = "TabletPreview", name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480")
//annotation class DevicePreviews

@Preview(name = "phone", group = "normal-xxhdpi", widthDp = 360, heightDp = 640)
@Preview(name = "landscape", group = "normal-xxhdpi", widthDp = 640, heightDp = 360)
@Preview(name = "foldable", group = "normal-xxhdpi", widthDp = 673, heightDp = 841)
@Preview(name = "tablet_ja", group = "ja-normal-xhdpi", widthDp = 1280, heightDp = 800)
@Preview(name = "tablet_ja_night", group = "ja-normal-night-xhdpi", widthDp = 1280, heightDp = 800)
annotation class DevicePreviews
2 changes: 1 addition & 1 deletion docs/handson/UILayerTest.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,4 @@ UIテストには、コード変更前後の画面スクリーンショットを
- Jetpack Composeの画面スクリーンショットを使ってVisual Regression Testを実現する
- [Composeのプレビュー画面でVisual Regression Testを行う](./VisualRegressionTest_Preview.md)
- [Visual Regression TestをCIで実行する](./VisualRegressionTest_CI.md)
- プレビューでは確認できないComposeの画面のスクリーンショットを撮る
- [様々なケースでComposeの画面スクリーンショットを撮る](./VisualRegressionTest_Advanced.md)
Loading

0 comments on commit f817d46

Please sign in to comment.