Share
Embed
Understand the mechanism! Let's do screenshots tests of Compose Previews with various variations / 仕組みから理解する!Composeプレビューを様々なバリエーションでスクリーンショットテストしよう
DroidKaigi 2024のセッション「仕組みから理解する!Composeプレビューを様々なバリエーションでスクリーンショットテストしよう」の発表資料です。
同時通訳される予定のため、スライドは英語で書かれています。
手許でご覧になりたいときにご活用ください。
また、サンプルコードと解説を「Composable Preview Scannerを使ってプレビュー画面のスクリーンショットを撮る」に公開していますので、あわせてご参照下さい。
TOYAMA Sumio
September 12, 2024
Transcript
141 slides
-
仕組みから理解する! Composeプレビューを様々なバリエーションで スクリーンショットテストしよう 2024.09.12 (DroidKaigi 2024) Sumio Toyama (sumio_tym) Understand
the mechanism! Let's do screenshots tests of Compose Previews with various variations -
About Me 2 Sumio Toyama (外山 純生) @sumio_tym (X)
/ @sumio (GitHub) Work at DeNA SWET Group on Android App Development / Test Automation (SWET = Software Engineer in Test) -
Talk Summary 3 Explain how to take various screenshots of
Composable Preview Functions using Roborazzi for Visual Regression Tests (VRT). Will also explain the roles of related tools and mechanisms to make it easier for everyone to implement. -
Topics Not Covered in This Session 4 Operation of
the screenshots taken Timing for taking the screenshots Selection of image comparison tools for VRT Building and operating CI (Continuous Integration) See https://github.com/DeNA/android-modern-architecture-test-handson/blob/main/docs/handson/VisualRegressionTest_CI.md for CI operation know-how. -
Agenda 5 1. Background (What We Aim to Achieve) 2.
Roles of Related Tools 3. Taking Screenshots of Composable Preview Functions 4. Capturing Various Types of Screenshots 5. Differences from Google's Compose Preview Screenshot Testing Methodology 6. Conclusion -
Agenda 6 1. Background (What We Aim to Achieve) 2.
Roles of Related Tools 3. Taking Screenshots of Composable Preview Functions 4. Capturing Various Types of Screenshots 5. Differences from Google's Compose Preview Screenshot Testing Methodology 6. Conclusion -
Definitions of Terms 7 Screenshot Test Automated testing, such
as saving screenshots of an application A human must visually verify the correctness of the saved screenshots -
8 Visual Regression Test (VRT) Automated test that compares
the results of screenshot tests to each other to ensure that there are no unintended differences in the screenshot images A visual check by a human is still required to see if the difference is intentional or not Screenshot testing tools often also provide diff detection Definitions of Terms -
History of screenshot testing 9 The Age of Android View
Write test code to interact with the UI and show the desired screen Take the screenshots by using Instrumented Test Hard to keep it operational ◆ Hard to write test code ◆ Tests tend to flaky due to async processing ◆ Very slow to execute on real devices and/or emulators and difficult to incorporate into CI -
10 The Age of Jetpack Compose The introduction of
the Preview function has led to the habit of checking screen layouts on the fly Saving a screenshot of the Preview function has also become popular It's now possible to take screenshots in Local Test The foundation for easy operation has been established! ◆ No test code is required with the Preview function ◆ Easily integrated into CI since it can be executed quickly in the JVM History of screenshot testing -
What We Aim to Achieve in This Session 11
Implement screenshot test based on the screenshot of the Preview function Take a screenshot of the various variations such as: Night mode and Light mode Tablet and Smartphone Various locales Write as little test code as possible -
Summary So Far 12 Clarified the difference between screenshot
testing and Visual Regression Tests With Jetpack Compose now in use, the foundation for easy operation of screenshot testing and VRT has been established Clarified what we want to achieve in this session -
Agenda 13 1. Background (What We Aim to Achieve) 2.
Roles of Related Tools 3. Taking Screenshots of Composable Preview Functions 4. Capturing Various Types of Screenshots 5. Differences from Google's Compose Preview Screenshot Testing Methodology 6. Conclusion -
What We Aim to Achieve (rough sketch) 14 @Preview @Preview
@Preview ・ ・ ・ @Test fun takeScreenShot() { ... } ①Collect the Preview Functions ②Take a screenshot of each Preview function collected ③And even take screenshots of various variations. (e.g., night mode, tablet screen, German locale, etc.) -
15 @Preview @Preview @Preview ・ ・ ・ @Test fun takeScreenShot()
{ ... } ①Collect the Preview Functions ②Take a screenshot of each Preview function collected ③And even take screenshots of various variations. (e.g., night mode, tablet screen, German locale, etc.) -
①Libs for Collecting Preview Functions 16 There are some
libraries for collecting preview functions In practice, one of these libraries will be chosen for your use -
①Libs for Collecting Preview Functions 17 Showkase https://github.com/airbnb/Showkase Library
that generates a UI catalog browser (ShowkaseBrowser) with Preview functions, fonts, colors, etc., and allows browsing on Android devices Provides API to retrieve the Preview functions (@Composable () -> Unit) collected by Showkase Only some properties of @Preview can be obtained Realized by using an annotation processor -
Code Snippet for Showkase 18 // In the app module
@ShowkaseRoot class MyShowkaseRootModule : ShowkaseRootModule // Retrieve the list of Preview composable functions val componentList: List<ShowkaseBrowserComponent> = Showkase.getMetadata().componentList val previewFunctionList: List<@Composable () -> Unit> = componentList.map { it.component } -
Note: Properties of ShowkaseBrowserComponent 19 Property Type Corresponding @Preview property
componentKey String group String group componentName String name componentKDoc String component @Composable () -> Unit styleName String? isDefaultStyle Boolean widthDp Int? widthDp heightDp Int? heightDp tags List<String> extraMetaData List<String> -
20 Property Type Corresponding @Preview property componentKey String group String
group componentName String name componentKDoc String component @Composable () -> Unit styleName String? isDefaultStyle Boolean widthDp Int? widthDp heightDp Int? heightDp tags List<String> extraMetaData List<String> Of the properties @Preview has, only these 4 can be handled by Showkase Note: Properties of ShowkaseBrowserComponent -
21 Composable Preview Scanner https://github.com/sergio-sastre/ComposablePreviewScanner Relatively new tool (1st
released on 2024-06-10) Library dedicated to collecting Preview functions and their metadata (e.g. properties of @Preview) Allows to access all @Preview properties Also allows to access other annotations attached to the @Preview function Realized by using the JVM's reflection functionality ①Libs for Collecting Preview Functions -
Code Snippet for Composable Preview Scanner 22 // Retrieve the
list of Preview composable functions val componentList: List<ComposablePreview<AndroidPreviewInfo>> = AndroidComposablePreviewScanner() .scanPackageTrees("com.example.droidkaigi", ...) .getPreviews() // ComposablePreview<T> implements a @Composable invoke(), so // you can call the preview function by calling invoke(). // If you intentionally store them in a List<@Composable () -> Unit> type, it would look like the following val previewFunctionList: List<@Composable () -> Unit> = composablePreviewList.map { { it() } } -
Note: Properties of AndroidPreviewInfo 23 Property Type Corresponding @Preview property
name String name group String group apiLevel Int apiLevel widthDp Int widthDp heightDp Int heightDp locale String locale fontScale Float fontScale showSystemUi Boolean showSystemUi showBackground Boolean showBackground backgroundColor Long backgroundColor uiMode Int uiMode device String device wallpaper Int wallpaper -
Note: Properties of AndroidPreviewInfo 24 Property Type Corresponding @Preview property
name String name group String group apiLevel Int apiLevel widthDp Int widthDp heightDp Int heightDp locale String locale fontScale Float fontScale showSystemUi Boolean showSystemUi showBackground Boolean showBackground backgroundColor Long backgroundColor uiMode Int uiMode device String device wallpaper Int wallpaper All properteis of @Preview can be handled -
Recap of ①Libs for Collecting Preview Functions 25 Introduced
2 libraries Showkase Composable Preview Scanner Basic functionality is the same for both Differ in the extent to which @Preview properties can be accessed -
26 @Preview @Preview @Preview ・ ・ ・ @Test fun takeScreenShot()
{ ... } ①Collect the Preview Functions ②Take a screenshot of each Preview function collected ③And even take screenshots of various variations. (e.g., night mode, tablet screen, German locale, etc.) -
②Libs for Taking Screenshot 27 Taking the screenshots is
achieved by combining multiple libraries All of the libraries introduced here are required -
②Libs for Taking Screenshot 28 ComposeTestRule https://developer.android.com/develop/ui/compose/testing/apis Official Jetpack
Compose Library Can test a Composable function unit by displaying on the screen UI interaction, layout validation, etc. -
29 Robolectric https://robolectric.org/ Allows to run Android tests as
Local Tests (on the JVM of your development PC) Executes Android Framework code as-is (as much as possible) in the JVM Also works with Jetpack Compose Recently, Graphics related classes are also executed as-is (Robolectric Native Graphics (RNG)) Hardware rendering is also supported to get more realistic rendering results ②Libs for Taking Screenshot -
30 Roborazzi https://takahirom.github.io/roborazzi/top.html Library for screenshot testing with Robolectric
Obtains screenshots closer to the actual device due to improved Robolectric functionality Supports Jetpack Compose (work with ComposeTestRule) ②Libs for Taking Screenshot -
Code Snippet for Screenshot Testing a Single Composable Func. 31
@RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class MyScreenshotTest { @get:Rule val composeTestRule = createComposeRule() @Test fun test() { composeTestRule.setContent { MyComposable() } composeTestRule.onRoot().captureRoboImage() }} -
Code Snippet for Screenshot Testing a Single Composable Func. 32
@RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class MyScreenshotTest { @get:Rule val composeTestRule = createComposeRule() @Test fun test() { composeTestRule.setContent { MyComposable() } composeTestRule.onRoot().captureRoboImage() }} Declaration to use Robolectric with Native Graphics enabled -
33 @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class MyScreenshotTest { @get:Rule val composeTestRule =
createComposeRule() @Test fun test() { composeTestRule.setContent { MyComposable() } composeTestRule.onRoot().captureRoboImage() }} Declaration of ComposeTestRule Code Snippet for Screenshot Testing a Single Composable Func. -
34 @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class MyScreenshotTest { @get:Rule val composeTestRule =
createComposeRule() @Test fun test() { composeTestRule.setContent { MyComposable() } composeTestRule.onRoot().captureRoboImage() }} Composable function under test Code Snippet for Screenshot Testing a Single Composable Func. -
35 @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class MyScreenshotTest { @get:Rule val composeTestRule =
createComposeRule() @Test fun test() { composeTestRule.setContent { MyComposable() } composeTestRule.onRoot().captureRoboImage() }} Take screenshot of MyComposable() Code Snippet for Screenshot Testing a Single Composable Func. -
Parameterized Test with Robolectric 36 @RunWith(ParameterizedRobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class MyParameterizedTest(private val
testCase: TestCase) { @Test fun test() { doSomeTest(testCase.text) } companion object { data class TestCase(private val text: String) @ParameterizedRobolectricTestRunner.Parameters @JvmStatic fun params() = listOf(TestCase("Hello"), TestCase("こんにちは")) }} -
Parameterized Test with Robolectric 37 @RunWith(ParameterizedRobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class MyParameterizedTest(private val
testCase: TestCase) { @Test fun test() { doSomeTest(testCase.text) } companion object { data class TestCase(private val text: String) @ParameterizedRobolectricTestRunner.Parameters @JvmStatic fun params() = listOf(TestCase("Hello"), TestCase("こんにちは")) }} ①Declare Runner for Parameterized Test -
Parameterized Test with Robolectric 38 @RunWith(ParameterizedRobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class MyParameterizedTest(private val
testCase: TestCase) { @Test fun test() { doSomeTest(testCase.text) } companion object { data class TestCase(private val text: String) @ParameterizedRobolectricTestRunner.Parameters @JvmStatic fun params() = listOf(TestCase("Hello"), TestCase("こんにちは")) }} ②Declare a list of parameters to be passed to the constructor of the test class -
Parameterized Test with Robolectric 39 @RunWith(ParameterizedRobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class MyParameterizedTest(private val
testCase: TestCase) { @Test fun test() { doSomeTest(testCase.text) } companion object { data class TestCase(private val text: String) @ParameterizedRobolectricTestRunner.Parameters @JvmStatic fun params() = listOf(TestCase("Hello"), TestCase("こんにちは")) }} ③A parameter passed as constructor argments -
Parameterized Test with Robolectric 40 @RunWith(ParameterizedRobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class MyParameterizedTest(private val
testCase: TestCase) { @Test fun test() { doSomeTest(testCase.text) } companion object { data class TestCase(private val text: String) @ParameterizedRobolectricTestRunner.Parameters @JvmStatic fun params() = listOf(TestCase("Hello"), TestCase("こんにちは")) }} ④Test with the parameter passed -
Recap of ②Libs for Taking Screenshot 41 Using a
combination of ComposeTestRule, Robolectric, and Roborazzi, a screenshot of the Composable function can be taken ParameterizedRobolectricTestRunner allows to run test for each element in the list of parameters used for testing -
Agenda 42 1. Background (What We Aim to Achieve) 2.
Roles of Related Tools 3. Taking Screenshots of Composable Preview Functions 4. Capturing Various Types of Screenshots 5. Differences from Google's Compose Preview Screenshot Testing Methodology 6. Conclusion -
Combining what we have so far... 43 If we can
prepare a list of preview functions to take screenshots, we should be able to take screenshots of each preview function by using ParameterizedRobolectricTestRunner -
44 @Preview @Preview @Preview ・ ・ ・ @Test fun takeScreenShot()
{ ... } ①Collect the Preview Functions ②Take a screenshot of each Preview function collected ③And even take screenshots of various variations. (e.g., night mode, tablet screen, German locale, etc.) -
Preparation for using Robolectric and Roborazzi 45 plugins { id("io.github.takahirom.roborazzi")
version "1.26.0" apply false } Top-level build.gradle.kts # Fix the screenshot output directory roborazzi.record.filePathStrategy=relativePathFromRoborazziContextOutputDi rectory # Save screenshots when running tests in Android Studio roborazzi.test.record=true gradle.properties -
Preparation for using Robolectric and Roborazzi 46 plugins { id("io.github.takahirom.roborazzi")
} android { testOptions { unitTests { isIncludeAndroidResources = true all { it.systemProperties( "roborazzi.output.dir" to rootProject.file("screenshots").absolutePath, "robolectric.pixelCopyRenderMode" to "hardware" ) } } } } build.gradle.kts file in the app module -
Preparation for using Robolectric and Roborazzi 47 plugins { id("io.github.takahirom.roborazzi")
} android { testOptions { unitTests { isIncludeAndroidResources = true all { it.systemProperties( "roborazzi.output.dir" to rootProject.file("screenshots").absolutePath, "robolectric.pixelCopyRenderMode" to "hardware" ) } } } } build.gradle.kts file in the app module Declarations required to use Robolectric -
Preparation for using Robolectric and Roborazzi 48 plugins { id("io.github.takahirom.roborazzi")
} android { testOptions { unitTests { isIncludeAndroidResources = true all { it.systemProperties( "roborazzi.output.dir" to rootProject.file("screenshots").absolutePath, "robolectric.pixelCopyRenderMode" to "hardware" ) } } } } build.gradle.kts file in the app module Specify the output directory for screenshots -
Preparation for using Robolectric and Roborazzi 49 dependencies { 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") } roborazzi { outputDir.set(rootProject.file("screenshots")) } build.gradle.kts file in the app module (cont.) -
Preparation for using Robolectric and Roborazzi 50 dependencies { 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") } roborazzi { outputDir.set(rootProject.file("screenshots")) } build.gradle.kts file in the app module (cont.) Specify the output directory for screenshots here as well -
Additional Preparation for using Showkase 51 plugins { id("com.google.devtools.ksp") version
"1.9.20-1.0.14" apply false } Top-level build.gradle.kts plugins { id("com.google.devtools.ksp") } dependencies { implementation("com.airbnb.android:showkase:1.0.3") ksp("com.airbnb.android:showkase-processor:1.0.3") } ksp { arg("skipPrivatePreviews", "true") } build.gradle.kts file in all modules in which @Preview function is used -
Additional Preparation for using Showkase 52 plugins { id("com.google.devtools.ksp") version
"1.9.20-1.0.14" apply false } Top-level build.gradle.kt plugins { id("com.google.devtools.ksp") } dependencies { implementation("com.airbnb.android:showkase:1.0.3") ksp("com.airbnb.android:showkase-processor:1.0.3") } ksp { arg("skipPrivatePreviews", "true") } build.gradle.kts file in all modules in which @Preview function is used Configuration to ignore private @Preview function without error -
Additional Preparation for using Showkase 53 @ShowkaseRoot class MyRootModule: ShowkaseRootModule
Declare empty class in the app module Any class name is allowed @ShowkaseRoot annotation is required Must inherit ShowkaseRootModule -
Code Snippet by using Showkase (1/3) 54 @RunWith(ParameterizedRobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config(qualifiers
= RobolectricDeviceQualifiers.Pixel7) class ShowkasePreviewTest( private val component: ShowkaseBrowserComponent ) { @get:Rule val composeTestRule = createComposeRule() ... -
Code Snippet by using Showkase (1/3) 55 @RunWith(ParameterizedRobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config(qualifiers
= RobolectricDeviceQualifiers.Pixel7) class ShowkasePreviewTest( private val component: ShowkaseBrowserComponent ) { @get:Rule val composeTestRule = createComposeRule() ... Use Parameterized Test -
Code Snippet by using Showkase (1/3) 56 @RunWith(ParameterizedRobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config(qualifiers
= RobolectricDeviceQualifiers.Pixel7) class ShowkasePreviewTest( private val component: ShowkaseBrowserComponent ) { @get:Rule val composeTestRule = createComposeRule() ... Test under Pixel7 screen -
Code Snippet by using Showkase (1/3) 57 @RunWith(ParameterizedRobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config(qualifiers
= RobolectricDeviceQualifiers.Pixel7) class ShowkasePreviewTest( private val component: ShowkaseBrowserComponent ) { @get:Rule val composeTestRule = createComposeRule() ... Passed data collected by Showkase as parameters -
Code Snippet by using Showkase (2/3) 58 @Test fun test()
{ val filePath = "${component.componentKey}.png" composeTestRule.setContent { CompositionLocalProvider( LocalInspectionMode provides true ) { component.component() } } composeTestRule.onRoot().captureRoboImage(filePath) } ... -
Code Snippet by using Showkase (2/3) 59 @Test fun test()
{ val filePath = "${component.componentKey}.png" composeTestRule.setContent { CompositionLocalProvider( LocalInspectionMode provides true ) { component.component() } } composeTestRule.onRoot().captureRoboImage(filePath) } ... Avoid duplicate file names for screenshots -
Code Snippet by using Showkase (2/3) 60 @Test fun test()
{ val filePath = "${component.componentKey}.png" composeTestRule.setContent { CompositionLocalProvider( LocalInspectionMode provides true ) { component.component() } } composeTestRule.onRoot().captureRoboImage(filePath) } ... Ensure the same result as displaying @Preview in Android Studio -
Code Snippet by using Showkase (2/3) 61 @Test fun test()
{ val filePath = "${component.componentKey}.png" composeTestRule.setContent { CompositionLocalProvider( LocalInspectionMode provides true ) { component.component() } } composeTestRule.onRoot().captureRoboImage(filePath) } ... Preview function stored in ShowkaseBrowserComponent -
Code Snippet by using Showkase (3/3) 62 companion object {
@ParameterizedRobolectricTestRunner.Parameters @JvmStatic fun params(): Iterable<ShowkaseBrowserComponent> = Showkase.getMetadata().componentList } } } -
Code Snippet by using Showkase (3/3) 63 companion object {
@ParameterizedRobolectricTestRunner.Parameters @JvmStatic fun params(): Iterable<ShowkaseBrowserComponent> = Showkase.getMetadata().componentList } } } Returns a list of ShowkaseBrowserComponent collected by Showkase -
Additional Notes for the Code Snippet 64 @Config(qualifiers = ...)
Robolectric's function to specify screen size, etc. filePath = "${component.componentKey}.png" componentKey: A value that uniquely distinguishes ShowkaseBrowserComponent Prevents screenshots of different Preview functions from being overwritten LocalInspectionMode This value is set to true when previewing in Android Studio Set to true if you want the result to be the same as the Android Studio preview -
Using Composable Preview Scanner 65 The concept is the
same as Showkase, but Different test parameter acquisition for Composable Preview Scanner Need to use AndroidPreviewScreenshotIdBuilder to specify screenshot file name Roborazzi Gradle Plugin provides a mechanism to simplify these, so use it as-is or customize it to your needs -
Additional Preparation for Using Composable Preview Scanner 66 dependencyResolutionManagement {
repositories { ... maven { url = URI("https://jitpack.io") } } } settings.gradle.kts -
Additional Preparation for Using Composable Preview Scanner 67 dependencies {
testImplementation("io.github.takahirom.roborazzi:roborazzi-compose- preview-scanner-support:1.26.0") testImplementation("com.github.sergio- sastre.ComposablePreviewScanner:android:0.3.0") } build.gradle.kts in the app module -
Additional Preparation for Using Composable Preview Scanner 68 roborazzi {
... @OptIn(ExperimentalRoborazziApi::class) generateComposePreviewRobolectricTests { enable = true // Specifies packages to scan preview functions packages = listOf("com.example.droidkaigi") } } build.gradle.kts in the app module -
Running Test by Using Composable Preview Scanner 69 The
build will generate a class called RoborazziPreviewParameterizedTests, which is used to run tests The following @Preview properties are supported by default uiMode (only night mode / light mode) locale fontScale -
70 If you combine the Preview function collector with
the ParameterizedRobolectricTestRunner, you can test screenshots of all collected Preview functions If you use Composable Preview Scanner, you can automatically generate screenshot tests just by setting up a build scripts Summary So Far -
Agenda 71 1. Background (What We Aim to Achieve) 2.
Roles of Related Tools 3. Taking Screenshots of Composable Preview Functions 4. Capturing Various Types of Screenshots 5. Differences from Google's Compose Preview Screenshot Testing Methodology 6. Conclusion -
72 @Preview @Preview @Preview ・ ・ ・ @Test fun takeScreenShot()
{ ... } ①Collect the Preview Functions ②Take a screenshot of each Preview function collected ③And even take screenshots of various variations. (e.g., night mode, tablet screen, German locale, etc.) -
Review of @Preview annotations @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES ) 73
The screenshot is from Now in Android App licensed under Apache License (Version 2.0). https://github.com/android/nowinandroid Various variations of the preview image can be checked by adding optional parameters to @Preview -
74 The screenshot is from Now in Android App licensed
under Apache License (Version 2.0). https://github.com/android/nowinandroid @Preview(fontScale = 2f) Review of @Preview annotations -
75 Multiple @Previews can be specified @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(fontScale = 2f) @Composable fun PreviewMyComposable() { ... } Custom annotations can be defined to group multiple @Previews @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) annotation class NightModeVariationPreview @NightModeVariationPreview @Composable fun PreviewMyComposable() { ... } Review of @Preview annotations -
Strategy for taking various screenshots 76 Take screenshots as
indicated in @Preview's property If the custom annotation shown on the previous slide is used, take 2 screenshots, night mode and light mode Can be achieved by changing the configs for taking screenshot based on the @Preview's property just before calling each captureRoboImage() @NightModeVariationPreview @Composable fun PreviewMyComposable() { ... } -
77 Custom annotations can be useful for taking multiple
patterns of screenshots uniformly for all @Preview functions If you want to take screenshots in night mode for all Preview functions, simply add the @NightModeVariationPreview to all Preview functions instead of the normal @Preview Strategy for taking various screenshots -
78 The strategy can be realized if the following
are implemented Access to the @Preview properties of the collected functions Take screenshots in various configuration (night mode, font scale, etc.) Strategy for taking various screenshots -
Access to @Preview properties 79 Showkase Only group,
name, widthDp, heightDp can be accessed To pass other optional information, it is necessary to encode it into a group property, etc. Example: @Preview(group="ja-night", locale = "ja", uiMode = UI_MODE_NIGHT_YES) Composable Preview Scanner All properties can be accessed If you want to use the full @Preview information, this library is recommended -
Take screenshots in various configuration 80 Available means are
as follows Original functionality of Jetpack Compose Functions of Robolectric Functions of ComposeTestRule -
Changing config using Compose's original functionality 81 Use of
CompositionLocalProvider @Test fun test() { composeTestRule.setContent { val density = LocalDensity.current val newDensity = Density( density = density.density, fontScale = density.fontScale * fontScale ) CompositionLocalProvider(LocalDensity provides newDensity) { myComposable() } } composeTestRule.onRoot().captureRoboImage() } Assume this fontScale is given externally -
@Test fun test() { composeTestRule.setContent { val density = LocalDensity.current
val newDensity = Density( density = density.density, fontScale = density.fontScale * fontScale ) CompositionLocalProvider(LocalDensity provides newDensity) { myComposable() } } composeTestRule.onRoot().captureRoboImage() } 82 Override LocalDensity to change font scale Changing config using Compose's original functionality Use of CompositionLocalProvider -
Changing config using Robolectric's functionality 83 Use of RuntimeEnvironment.setQualifiers()
Configuration specified in @Config can be changed dynamically @RunWith(ParameterizedRobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config(qualifiers = RobolectricDeviceQualifiers.Pixel7) class MyTest(private val testCase: TestCase) { ... -
84 @RunWith(ParameterizedRobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config(qualifiers = RobolectricDeviceQualifiers.Pixel7) class MyTest(private val testCase:
TestCase) { ... "w411dp-h914dp-normal-long-notround-any-420dpi-keyshidden-nonav" Changing config using Robolectric's functionality Use of RuntimeEnvironment.setQualifiers() Configuration specified in @Config can be changed dynamically -
Note: Possible values for @Config qualifiers 85 Configuration Qualifier values
(Example) Description Locale en, en-US, fr-rCA, ... language code / region code Layout Direction ldltr, ldrtl left-to-right / right-to-left Screen Orientation port, land portrait / landscape orientation Night Mode night, notnight Night Mode (Dark Mode) Same as Android Resource Directory naming rule(e.g., values-ja) https://developer.android.com/guide/topics/resources/providing-resources.html#QualifierRules Specify from the top of the table referenced from the URL above Example: land-night (Landscape and Night Mode) -
Changing config using Robolectric's functionality 86 Use of RuntimeEnvironment.setQualifiers()
A qualifier with a leading + can be used to override a part of configuration declared in @Config @Config(qualifiers = "xlarge-port") class MyTest { @get:Rule val composeTestRule = createAndroidComposeRule<ComponentActivity>() ... @Test fun testLandscape() { RuntimeEnvironment.setQualifiers("+land") composeTestRule.activityRule.scenario.recreate() // become xlarge-land (Landscape mode) here }} -
Changing config using Robolectric's functionality 87 @Config(qualifiers = "xlarge-port") class
MyTest { @get:Rule val composeTestRule = createAndroidComposeRule<ComponentActivity>() ... @Test fun testLandscape() { RuntimeEnvironment.setQualifiers("+land") composeTestRule.activityRule.scenario.recreate() // become xlarge-land (Landscape mode) here }} Changed from xlarge-port to xlarge-land Use of RuntimeEnvironment.setQualifiers() A qualifier with a leading + can be used to override a part of configuration declared in @Config -
88 @Config(qualifiers = "xlarge-port") class MyTest { @get:Rule val composeTestRule
= createAndroidComposeRule<ComponentActivity>() ... @Test fun testLandscape() { RuntimeEnvironment.setQualifiers("+land") composeTestRule.activityRule.scenario.recreate() // become xlarge-land (Landscape mode) here }} Remember to recreate the Activity to apply changes Changing config using Robolectric's functionality Use of RuntimeEnvironment.setQualifiers() A qualifier with a leading + can be used to override a part of configuration declared in @Config -
89 @Config(qualifiers = "xlarge-port") class MyTest { @get:Rule val composeTestRule
= createAndroidComposeRule<ComponentActivity>() ... @Test fun testLandscape() { RuntimeEnvironment.setQualifiers("+land") composeTestRule.activityRule.scenario.recreate() // become xlarge-land (Landscape mode) here }} Changing config using Robolectric's functionality Use this instead of createComposeRule to allow access to activityRule Use of RuntimeEnvironment.setQualifiers() A qualifier with a leading + can be used to override a part of configuration declared in @Config -
90 @get:Rule val composeTestRule = createAndroidComposeRule<ComponentActivity>() ... @Test fun test()
{ // Assume that widthDp and heightDp are given externally setDisplaySize(widthDp, heightDp) composeTestRule.activityRule.scenario.recreate() composeTestRule.setContent { ... } composeTestRule.onRoot().captureRoboImage() } Use of ShadowDisplay for changing screen size Changing config using Robolectric's functionality -
91 @get:Rule val composeTestRule = createAndroidComposeRule<ComponentActivity>() ... @Test fun test()
{ // Assume that widthDp and heightDp are given externally setDisplaySize(widthDp, heightDp) composeTestRule.activityRule.scenario.recreate() composeTestRule.setContent { ... } composeTestRule.onRoot().captureRoboImage() } will be explained in later slides Use of ShadowDisplay for changing screen size Changing config using Robolectric's functionality -
92 @get:Rule val composeTestRule = createAndroidComposeRule<ComponentActivity>() ... @Test fun test()
{ // Assume widthDp and heightDp are given externally setDisplaySize(widthDp, heightDp) composeTestRule.activityRule.scenario.recreate() composeTestRule.setContent { ... } composeTestRule.onRoot().captureRoboImage() } Recreate the Activity to apply changes Use of ShadowDisplay for changing screen size Changing config using Robolectric's functionality -
93 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) } } Use of ShadowDisplay for changing screen size Changing config using Robolectric's functionality -
94 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) } } Specify new screen size (in pixels) Use of ShadowDisplay for changing screen size Changing config using Robolectric's functionality -
Status of @Preview properties support so far 95 @Preview Property
Tool to use API to use name - - group - - apiLevel - - widthDp Robolectric ShadowDisplay heightDp Robolectric ShadowDisplay locale Robolectric setQualifiers("+ja") fontScale LocalCompositionProvider LocalDensity showSystemUi - - showBackground - - backgroundColor - - uiMode Robolectric setQualifiers("+night") / setQualifiers("+television") device Robolectric setQualifiers(RobolectricDeviceQualifiers.Pixel7) wallpaper - - -
Note: Font scale can also be changed by Robolectric 96
@Test fun test() { val fontScale = ... RuntimeEnvironment.setFontScale(fontScale) composeTestRule.activityRule.scenario.recreate() composeTestRule.setContent { myComposable() } composeTestRule.onRoot().captureRoboImage() } -
Changing config using ComposeTestRule's functionality 97 Can control the
time at which a screenshot is taken Useful for taking a screenshot when the automatically started animation is completed @Test fun test() { composeTestRule.apply { mainClock.autoAdvance = false setContent { ... } mainClock.advanceTimeBy(1_000) // animation playback duration onRoot().captureRoboImage() mainClock.autoAdvance = true } } -
98 @Test fun test() { composeTestRule.apply { mainClock.autoAdvance = false
setContent { ... } mainClock.advanceTimeBy(1_000) // animation playback duration onRoot().captureRoboImage() mainClock.autoAdvance = true } } Declaration to manually control the time governed by composeTestRule Changing config using ComposeTestRule's functionality Can control the time at which a screenshot is taken Useful for taking a screenshot when the automatically started animation is completed -
99 @Test fun test() { composeTestRule.apply { mainClock.autoAdvance = false
setContent { ... } mainClock.advanceTimeBy(1_000) // animation playback duration onRoot().captureRoboImage() mainClock.autoAdvance = true } } Advance the clock to the point where you want to take a screenshot Changing config using ComposeTestRule's functionality Can control the time at which a screenshot is taken Useful for taking a screenshot when the automatically started animation is completed -
100 @Test fun test() { composeTestRule.apply { mainClock.autoAdvance = false
setContent { ... } mainClock.advanceTimeBy(1_000) // animation playback duration onRoot().captureRoboImage() mainClock.autoAdvance = true } } Restore the clock to automatic mode Changing config using ComposeTestRule's functionality Can control the time at which a screenshot is taken Useful for taking a screenshot when the automatically started animation is completed -
Code Snippet of Composable Preview Scanner 101 Here is
how to take a screenshot as indicated in the properties below Night mode or light mode (uiMode in @Preview) Screen size (widthDp, heightDp in @Preview) Font scale (fontScale in @Preview) Screenshot after a specific time has elapsed If you want to take a screenshot after a specific time has elapsed, add @DelayedPreview as follows @Preview @DelayedPreview(delay = 2000) @Composable fun PreviewMyAnimationComposable() { ... } -
ComposePreviewTester 102 Roborazzi provides a ComposePreviewTester interface, which wraps
ParameterizedRobolectricTestRunner We'll implement the interface interface ComposePreviewTester<T : Any> { fun options(): Options = defaultOptionsFromPlugin fun previews(): List<ComposablePreview<T>> fun test(preview: ComposablePreview<T>) } -
103 interface ComposePreviewTester<T : Any> { fun options(): Options =
defaultOptionsFromPlugin fun previews(): List<ComposablePreview<T>> fun test(preview: ComposablePreview<T>) } Used to customize options passed from build.gradle.kts You can also specify JUnit4 Rule to inject ComposePreviewTester Roborazzi provides a ComposePreviewTester interface, which wraps ParameterizedRobolectricTestRunner We'll implement the interface -
104 interface ComposePreviewTester<T : Any> { fun options(): Options =
defaultOptionsFromPlugin fun previews(): List<ComposablePreview<T>> fun test(preview: ComposablePreview<T>) } Implement to return a list of preview functions collected by Composable Preview Scanner ComposePreviewTester Roborazzi provides a ComposePreviewTester interface, which wraps ParameterizedRobolectricTestRunner We'll implement the interface -
105 interface ComposePreviewTester<T : Any> { fun options(): Options =
defaultOptionsFromPlugin fun previews(): List<ComposablePreview<T>> fun test(preview: ComposablePreview<T>) } Write test code for each preview function passwd as an argument ComposePreviewTester Roborazzi provides a ComposePreviewTester interface, which wraps ParameterizedRobolectricTestRunner We'll implement the interface -
Note: Caller of ComposePreviewTester 106 Caller of ComposePreviewTester is
automatically generated with the name RoborazziPreviewParameterizedTests @RunWith(ParameterizedRobolectricTestRunner::class) class RoborazziPreviewParameterizedTests( private val preview: ComposablePreview<Any> ) { // ↓ Instance of the impl class of ComposePreviewTester private val tester = ... ... @Test fun test() { tester.test(preview) } companion object { val previews by lazy { tester.previews() } // lazy for performance @JvmStatic @ParameterizedRobolectricTestRunner.Parameters fun values(): List<ComposablePreview<Any>> = previews } } Excerpts from main part only -
107 @RunWith(ParameterizedRobolectricTestRunner::class) class RoborazziPreviewParameterizedTests( private val preview: ComposablePreview<Any> ) {
// ↓ Instance of the impl class of ComposePreviewTester private val tester = ... ... @Test fun test() { tester.test(preview) } companion object { val previews by lazy { tester.previews() } // lazy for performance @JvmStatic @ParameterizedRobolectricTestRunner.Parameters fun values(): List<ComposablePreview<Any>> = previews } } Calling tester.test() for each element of tester.previews() Excerpts from main part only Note: Caller of ComposePreviewTester Caller of ComposePreviewTester is automatically generated with the name RoborazziPreviewParameterizedTests -
Implementation of ComposePreviewTester (1/6) 108 class MyPreviewTester: ComposePreviewTester<AndroidPreviewInfo> { val
composeTestRule = createAndroidComposeRule<ComponentActivity>() override fun options(): ComposePreviewTester.Options { val testLifecycleOptions = ComposePreviewTester.Options.JUnit4TestLifecycleOptions( testRuleFactory = { composeTestRule } ) return super.options().copy( testLifecycleOptions = testLifecycleOptions ) } -
109 class MyPreviewTester: ComposePreviewTester<AndroidPreviewInfo> { val composeTestRule = createAndroidComposeRule<ComponentActivity>() override
fun options(): ComposePreviewTester.Options { val testLifecycleOptions = ComposePreviewTester.Options.JUnit4TestLifecycleOptions( testRuleFactory = { composeTestRule } ) return super.options().copy( testLifecycleOptions = testLifecycleOptions ) } Declare composeTestRule (without @get:Rule) Implementation of ComposePreviewTester (1/6) -
110 class MyPreviewTester: ComposePreviewTester<AndroidPreviewInfo> { val composeTestRule = createAndroidComposeRule<ComponentActivity>() override
fun options(): ComposePreviewTester.Options { val testLifecycleOptions = ComposePreviewTester.Options.JUnit4TestLifecycleOptions( testRuleFactory = { composeTestRule } ) return super.options().copy( testLifecycleOptions = testLifecycleOptions ) } Specify the JUnit4 Rule to inject (in this case, composeTestRule) Implementation of ComposePreviewTester (1/6) -
Implementation of ComposePreviewTester (2/6) 111 override fun previews(): List<ComposablePreview<AndroidPreviewInfo>> {
val options = options() return AndroidComposablePreviewScanner() .scanPackageTrees(*options.scanOptions.packages.toTypedArray()) .includeAnnotationInfoForAllOf(DelayedPreview::class.java) .getPreviews() } -
Implementation of ComposePreviewTester (2/6) 112 override fun previews(): List<ComposablePreview<AndroidPreviewInfo>> {
val options = options() return AndroidComposablePreviewScanner() .scanPackageTrees(*options.scanOptions.packages.toTypedArray()) .includeAnnotationInfoForAllOf(DelayedPreview::class.java) .getPreviews() } The package passed to be scanned is specified in build.gradle.kts -
Implementation of ComposePreviewTester (2/6) 113 override fun previews(): List<ComposablePreview<AndroidPreviewInfo>> {
val options = options() return AndroidComposablePreviewScanner() .scanPackageTrees(*options.scanOptions.packages.toTypedArray()) .includeAnnotationInfoForAllOf(DelayedPreview::class.java) .getPreviews() } Get @DelayedPreview information as well -
Implementation of ComposePreviewTester (3/6) 114 override fun test(preview: ComposablePreview<AndroidPreviewInfo>) {
val delay = preview.getAnnotation<DelayedPreview>()?.delay ?: 0L val defaultFileName = AndroidPreviewScreenshotIdBuilder(preview) .build() val fileName = if (delay == 0L) defaultFileName else "${defaultFileName}_delay$delay" preview.myApplyToRobolectricConfiguration() composeTestRule.activityRule.scenario.recreate() composeTestRule.apply { ... } } -
Implementation of ComposePreviewTester (3/6) 115 override fun test(preview: ComposablePreview<AndroidPreviewInfo>) {
val delay = preview.getAnnotation<DelayedPreview>()?.delay ?: 0L val defaultFileName = AndroidPreviewScreenshotIdBuilder(preview) .build() val fileName = if (delay == 0L) defaultFileName else "${defaultFileName}_delay$delay" preview.myApplyToRobolectricConfiguration() composeTestRule.activityRule.scenario.recreate() composeTestRule.apply { ... } } Retrieve the delay property in @DelayedPreview -
Implementation of ComposePreviewTester (3/6) 116 override fun test(preview: ComposablePreview<AndroidPreviewInfo>) {
val delay = preview.getAnnotation<DelayedPreview>()?.delay ?: 0L val defaultFileName = AndroidPreviewScreenshotIdBuilder(preview) .build() val fileName = if (delay == 0L) defaultFileName else "${defaultFileName}_delay$delay" preview.myApplyToRobolectricConfiguration() composeTestRule.activityRule.scenario.recreate() composeTestRule.apply { ... } } Construct a unique file name -
Implementation of ComposePreviewTester (3/6) 117 override fun test(preview: ComposablePreview<AndroidPreviewInfo>) {
val delay = preview.getAnnotation<DelayedPreview>()?.delay ?: 0L val defaultFileName = AndroidPreviewScreenshotIdBuilder(preview) .build() val fileName = if (delay == 0L) defaultFileName else "${defaultFileName}_delay$delay" preview.myApplyToRobolectricConfiguration() composeTestRule.activityRule.scenario.recreate() composeTestRule.apply { ... } } Using Robolectric API to set up the configuration as indicated by the @Preview properties -
Implementation of ComposePreviewTester (4/6) 118 composeTestRule.apply { try { if
(delay != 0L) { mainClock.autoAdvance = false } setContent { ApplyToCompositionLocal(preview) { preview() } } if (delay != 0L) { mainClock.advanceTimeBy(delay) } onRoot().captureRoboImage(filePath = "$fileName.png") } finally { mainClock.autoAdvance = true } } } -
Implementation of ComposePreviewTester (4/6) 119 composeTestRule.apply { try { if
(delay != 0L) { mainClock.autoAdvance = false } setContent { ApplyToCompositionLocal(preview) { preview() } } if (delay != 0L) { mainClock.advanceTimeBy(delay) } onRoot().captureRoboImage(filePath = "$fileName.png") } finally { mainClock.autoAdvance = true } } } Control the time if delay property is specified -
Implementation of ComposePreviewTester (4/6) 120 composeTestRule.apply { try { if
(delay != 0L) { mainClock.autoAdvance = false } setContent { ApplyToCompositionLocal(preview) { preview() } } if (delay != 0L) { mainClock.advanceTimeBy(delay) } onRoot().captureRoboImage(filePath = "$fileName.png") } finally { mainClock.autoAdvance = true } } } Configure CompositionLocal as indicated by the @Preview properties -
Implementation of ComposePreviewTester (5/6) 121 fun ComposablePreview<AndroidPreviewInfo>.myApplyToRobolectricConfiguration() { val previewInfo
= this.previewInfo // ナイトモード when (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 (previewInfo.widthDp != -1 && previewInfo.heightDp != -1) { setDisplaySize(previewInfo.widthDp, previewInfo.heightDp) } } -
Implementation of ComposePreviewTester (5/6) 122 fun ComposablePreview<AndroidPreviewInfo>.myApplyToRobolectricConfiguration() { val previewInfo
= this.previewInfo // ナイトモード when (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 (previewInfo.widthDp != -1 && previewInfo.heightDp != -1) { setDisplaySize(previewInfo.widthDp, previewInfo.heightDp) } } Properties in @Preview are stored in previewInfo -
Implementation of ComposePreviewTester (5/6) 123 fun ComposablePreview<AndroidPreviewInfo>.myApplyToRobolectricConfiguration() { val previewInfo
= this.previewInfo // ナイトモード when (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 (previewInfo.widthDp != -1 && previewInfo.heightDp != -1) { setDisplaySize(previewInfo.widthDp, previewInfo.heightDp) } } Change configuration by using setQualifiers -
Implementation of ComposePreviewTester (5/6) 124 fun ComposablePreview<AndroidPreviewInfo>.myApplyToRobolectricConfiguration() { val previewInfo
= this.previewInfo // ナイトモード when (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 (previewInfo.widthDp != -1 && previewInfo.heightDp != -1) { setDisplaySize(previewInfo.widthDp, previewInfo.heightDp) } } Change screen size by using ShadowDisplay API -
Implementation of ComposePreviewTester (6/6) 125 @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() } } -
Implementation of ComposePreviewTester (6/6) 126 @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() } } Override LocalDensity to change font scale -
Settings of build.gradle.kts 127 LocalDensityを上書きして フォントスケールを変更 roborazzi { ... @OptIn(ExperimentalRoborazziApi::class)
generateComposePreviewRobolectricTests { enable = true // The class name of ComposePreviewTester implementation testerQualifiedClassName = "com.example.droidkaigi.MyPreviewTester" packages = listOf("com.example.droidkaigi") } } -
Summary So Far 128 Basic strategy is to take
screenshots with variations as specified in the @Preview properties There are 3 means of changing the configuration in which screenshots are taken CompositionLocalProvider Robolectric Time control by using ComposeTestRule Implement ComposePreviewTester interface provided by Roborazzi in case of using Compose Preview Scanner -
Agenda 129 1. Background (What We Aim to Achieve) 2.
Roles of Related Tools 3. Taking Screenshots of Composable Preview Functions 4. Capturing Various Types of Screenshots 5. Differences from Google's Compose Preview Screenshot Testing Methodology 6. Conclusion -
Google's Compose Preview Screenshot Testing 130 The following features
are available by applying the Gradle Plugin provided by Google Take a screenshot of the preview function placed in src/screenshotTest (not src/main) The @Preview property is automatically reflected in the screenshot VRT feature to compare images -
Compose Preview Screenshot Testing Setup Instructions 131 https://developer.android.com/studio/preview/compose-screenshot-testing Requirements
Android Gradle Plugin 8.5.0-beta01 or higher Kotlin 1.9.20 or higher Compose Compiler 1.5.4 or higher Versions used for this example Gradle 8.7 Android Gradle Plugin 8.5.2 Kotlin 1.9.20 Android Compose Screenshot Plugin 0.0.1-alpha05 -
132 https://developer.android.com/studio/preview/compose-screenshot-testing plugins { id("com.android.compose.screenshot") version "0.0.1-alpha05" } android {
experimentalProperties["android.experimental.enableScreenshotTest"] = true } dependencies { screenshotTestImplementation("androidx.compose.ui:ui-tooling") } build.gradle.kts in all modules Compose Preview Screenshot Testing Setup Instructions -
133 https://developer.android.com/studio/preview/compose-screenshot-testing plugins { id("com.android.compose.screenshot") version "0.0.1-alpha05" } android {
experimentalProperties["android.experimental.enableScreenshotTest"] = true } dependencies { screenshotTestImplementation("androidx.compose.ui:ui-tooling") } build.gradle.kts in all modules It is screenshotTestImplementation not testImplementation Compose Preview Screenshot Testing Setup Instructions -
134 https://developer.android.com/studio/preview/compose-screenshot-testing android.experimental.enableScreenshotTest=true gradle.properties Copy @Preview functions to
screenshotTest source set {module}/src/screenshotTest/java/.../ Compose Preview Screenshot Testing Setup Instructions -
Usage of Compose Preview Screenshot Testing 135 Generate reference
images Only save screenshots and don't create a test report ./gradlew updateDebugScreenshotTest Screenshots are saved in {module}/src/{variant}/screenshotTest/reference/ Generate test report Compare with the reference images and make a test report ./gradlew validateDebugScreenshotTest Test report are saved in {module}/build/reports/screenshotTest/preview/{variant}/index.html -
Difference from using Roborazzi and other methods 136 Usage
of Google's tool is very simple All you have to do is put the preview functions in src/screenshotTest and it will automatically take a screenshot for you Also support @Preview properties No test code is needed Maybe the preview function needs to remain in src/main as well Sometimes preview is not shown in src/screenshotTest in Android Studio Not very customizable If your use case is a good fit, you may start with Google's Compose Preview Screenshot Testing -
Agenda 137 1. Background (What We Aim to Achieve) 2.
Roles of Related Tools 3. Taking Screenshots of Composable Preview Functions 4. Capturing Various Types of Screenshots 5. Differences from Google's Compose Preview Screenshot Testing Methodology 6. Conclusion -
Wrap Up 138 Explained what each of the libraries
for taking screenshots can do and their roles Showed how to combine these libraries to take a screenshot of the preview functions Further explained how to take screenshots with different variations As additional information, gave an overview of Google's Compose Preview Screenshot Testing and explained how it differs from the libraries introduced in this session -
References 139 "Test your Compose layout" https://developer.android.com/develop/ui/compose/testing Robolectric
https://robolectric.org/ Roborazzi https://takahirom.github.io/roborazzi/top.html Showkase https://github.com/airbnb/Showkase Composable Preview Scanner https://github.com/sergio-sastre/ComposablePreviewScanner Google's Compose Preview Screenshot Testing https://developer.android.com/studio/preview/compose-screenshot-testing "Efficient Testing with Robolectric & Roborazzi Across Many UI States, Devices and Configurations" https://sergiosastre.hashnode.dev/efficient-testing-with-robolectric-roborazzi-across-many-ui- states-devices-and-configurations -
Finally 140 Please refer to this repository on GitHub for
the explanations and the code examples of this session (In Japanese) https://github.com/DeNA/android-modern-architecture-test- handson/blob/main/docs/handson/VisualRegressionTest_Previe w_ComposablePreviewScanner.md 「Composable Preview Scannerを使ってプレビュー画面のス クリーンショットを撮る」 -
141 With this session as a guide, let's leave the
tedious work of verifying different screen variations to screenshot testing Thank you for listening!