Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Understand the mechanism! Let's do screenshots ...

TOYAMA Sumio
September 12, 2024

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
Tweet

More Decks by TOYAMA Sumio

Other Decks in Programming

Transcript

  1. 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)
  2. 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.
  3. 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.
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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.)
  14. 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.)
  15. ①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
  16. ①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
  17. 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 }
  18. 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>
  19. 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
  20. 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
  21. 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() } }
  22. 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
  23. 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
  24. 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
  25. 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.)
  26. ②Libs for Taking Screenshot 27  Taking the screenshots is

    achieved by combining multiple libraries  All of the libraries introduced here are required
  27. ②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.
  28. 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
  29. 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
  30. 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() }}
  31. 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
  32. 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.
  33. 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.
  34. 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.
  35. 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("こんにちは")) }}
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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.)
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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.)
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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() ...
  54. 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
  55. 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
  56. 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
  57. 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) } ...
  58. 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
  59. 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
  60. 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
  61. Code Snippet by using Showkase (3/3) 62 companion object {

    @ParameterizedRobolectricTestRunner.Parameters @JvmStatic fun params(): Iterable<ShowkaseBrowserComponent> = Showkase.getMetadata().componentList } } }
  62. 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
  63. 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
  64. 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
  65. Additional Preparation for Using Composable Preview Scanner 66 dependencyResolutionManagement {

    repositories { ... maven { url = URI("https://jitpack.io") } } }  settings.gradle.kts
  66. 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
  67. 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
  68. 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
  69. 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
  70. 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
  71. 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.)
  72. 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
  73. 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
  74. 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
  75. 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() { ... }
  76. 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
  77. 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
  78. 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
  79. Take screenshots in various configuration 80  Available means are

    as follows  Original functionality of Jetpack Compose  Functions of Robolectric  Functions of ComposeTestRule
  80. 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
  81. @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
  82. 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) { ...
  83. 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
  84. 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)
  85. 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 }}
  86. 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
  87. 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
  88. 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
  89. 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
  90. 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
  91. 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
  92. 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
  93. 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
  94. 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 - -
  95. 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() }
  96. 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 } }
  97. 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
  98. 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
  99. 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
  100. 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() { ... }
  101. 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>) }
  102. 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
  103. 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
  104. 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
  105. 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
  106. 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
  107. 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 ) }
  108. 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)
  109. 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)
  110. 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() }
  111. 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
  112. 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
  113. 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 { ... } }
  114. 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
  115. 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
  116. 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
  117. 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 } } }
  118. 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
  119. 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
  120. 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) } }
  121. 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
  122. 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
  123. 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
  124. 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() } }
  125. 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
  126. 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") } }
  127. 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
  128. 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
  129. 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
  130. 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
  131. 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
  132. 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
  133. 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
  134. 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
  135. 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
  136. 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
  137. 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
  138. 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
  139. 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を使ってプレビュー画面のス クリーンショットを撮る」
  140. 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!