diff --git a/.gitignore b/.gitignore index ea4a4521..95d393ff 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ .idea/modules /.intellijPlatform/ .idea/runConfigurations.xml +.idea/shelf *.iml *.ipr .DS_Store diff --git a/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RoborazziPreviewScannerSupport.kt b/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RoborazziPreviewScannerSupport.kt index f7128999..afea762b 100644 --- a/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RoborazziPreviewScannerSupport.kt +++ b/roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RoborazziPreviewScannerSupport.kt @@ -13,49 +13,52 @@ import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview fun ComposablePreview.captureRoboImage( filePath: String, roborazziOptions: RoborazziOptions = provideRoborazziContext().options, - configBuilder: RoborazziComposeConfigBuilder = this.toRoborazziComposeConfigBuilder() + roborazziComposeOptions: RoborazziComposeOptions = this.toRoborazziComposeOptions() ) { if (!roborazziOptions.taskType.isEnabled()) return val composablePreview = this - captureRoboImage(filePath, roborazziOptions, configBuilder) { + captureRoboImage(filePath, roborazziOptions, roborazziComposeOptions) { composablePreview() } } @ExperimentalRoborazziApi -fun ComposablePreview.toRoborazziComposeConfigBuilder() = - RoborazziComposeConfigBuilder() - .size( +fun ComposablePreview.toRoborazziComposeOptions(): RoborazziComposeOptions { + return RoborazziComposeOptions { + size( widthDp = previewInfo.widthDp, heightDp = previewInfo.heightDp ) - .background( + background( showBackground = previewInfo.showBackground, backgroundColor = previewInfo.backgroundColor ) - .locale(previewInfo.locale) - .uiMode(previewInfo.uiMode) - .previewDevice(previewInfo.device) - .fontScale(previewInfo.fontScale) + locale(previewInfo.locale) + uiMode(previewInfo.uiMode) + previewDevice(previewInfo.device) + fontScale(previewInfo.fontScale) + } +} @Suppress("UnusedReceiverParameter") @Deprecated( - message = "Use previewInfo.toRoborazziComposeConfigBuilder().apply(scenario, composeContent) or ComposablePreview.captureRoboImage() instead", - replaceWith = ReplaceWith("previewInfo.toRoborazziComposeConfigBuilder().apply(scenario, composeContent)"), + message = "Use previewInfo.toRoborazziComposeOptions().apply(scenario, composeContent) or ComposablePreview.captureRoboImage() instead", + replaceWith = ReplaceWith("previewInfo.toRoborazziComposeOptions().apply(scenario, composeContent)"), level = DeprecationLevel.ERROR ) fun ComposablePreview.applyToRobolectricConfiguration() { - throw UnsupportedOperationException("Use previewInfo.toRoborazziComposeConfigBuilder().apply(scenario, composeContent) or ComposablePreview.captureRoboImage() instead") + throw UnsupportedOperationException("Use previewInfo.toRoborazziComposeOptions().apply(scenario, composeContent) or ComposablePreview.captureRoboImage() instead") } @ExperimentalRoborazziApi -fun RoborazziComposeConfigBuilder.previewDevice(previewDevice: String) = - with(RoborazziComposePreviewDeviceConfig(previewDevice)) +fun RoborazziComposeOptions.Builder.previewDevice(previewDevice: String): RoborazziComposeOptions.Builder { + return addOption(RoborazziComposePreviewDeviceOption(previewDevice)) +} @ExperimentalRoborazziApi -data class RoborazziComposePreviewDeviceConfig(private val previewDevice: String) : - RoborazziComposeSetupConfig { +data class RoborazziComposePreviewDeviceOption(private val previewDevice: String) : + RoborazziComposeSetupOption { override fun configure() { if (previewDevice.isNotBlank()) { // Requires `io.github.sergio-sastre.ComposablePreviewScanner:android:0.4.0` or later diff --git a/roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziCompose.kt b/roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziCompose.kt index b3a1f4ff..a2195538 100644 --- a/roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziCompose.kt +++ b/roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziCompose.kt @@ -30,7 +30,7 @@ fun captureRoboImage( captureRoboImage( file = file, roborazziOptions = roborazziOptions, - configBuilder = RoborazziComposeConfigBuilder(), + roborazziComposeOptions = RoborazziComposeOptions(), content = content ) } @@ -39,13 +39,13 @@ fun captureRoboImage( fun captureRoboImage( filePath: String = DefaultFileNameGenerator.generateFilePath(), roborazziOptions: RoborazziOptions = provideRoborazziContext().options, - configBuilder: RoborazziComposeConfigBuilder = RoborazziComposeConfigBuilder(), + roborazziComposeOptions: RoborazziComposeOptions = RoborazziComposeOptions(), content: @Composable () -> Unit, ) { captureRoboImage( file = fileWithRecordFilePathStrategy(filePath), roborazziOptions = roborazziOptions, - configBuilder = configBuilder, + roborazziComposeOptions = roborazziComposeOptions, content = content ) } @@ -54,13 +54,13 @@ fun captureRoboImage( fun captureRoboImage( file: File, roborazziOptions: RoborazziOptions = provideRoborazziContext().options, - configBuilder: RoborazziComposeConfigBuilder = RoborazziComposeConfigBuilder(), + roborazziComposeOptions: RoborazziComposeOptions = RoborazziComposeOptions(), content: @Composable () -> Unit, ) { if (!roborazziOptions.taskType.isEnabled()) return launchRoborazziTransparentActivity { activityScenario -> - val configuredContent = configBuilder - .configure(activityScenario) { + val configuredContent = roborazziComposeOptions + .configured(activityScenario) { content() } activityScenario.captureRoboImage(file, roborazziOptions) { diff --git a/roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposeConfig.kt b/roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposeOptions.kt similarity index 55% rename from roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposeConfig.kt rename to roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposeOptions.kt index 5feed029..51b6daa5 100644 --- a/roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposeConfig.kt +++ b/roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposeOptions.kt @@ -18,72 +18,101 @@ import org.robolectric.shadows.ShadowDisplay.getDefaultDisplay import kotlin.math.roundToInt @ExperimentalRoborazziApi -interface RoborazziComposeConfig +interface RoborazziComposeOption @ExperimentalRoborazziApi -interface RoborazziComposeSetupConfig : RoborazziComposeConfig { +interface RoborazziComposeSetupOption : RoborazziComposeOption { fun configure() } @ExperimentalRoborazziApi -interface RoborazziComposeActivityScenarioConfig : RoborazziComposeConfig { +interface RoborazziComposeActivityScenarioOption : RoborazziComposeOption { fun configureWithActivityScenario(scenario: ActivityScenario) } @ExperimentalRoborazziApi -interface RoborazziComposeComposableConfig : RoborazziComposeConfig { +interface RoborazziComposeComposableOption : RoborazziComposeOption { fun configureWithComposable(content: @Composable () -> Unit): @Composable () -> Unit } @ExperimentalRoborazziApi -class RoborazziComposeConfigBuilder { - private val activityScenarioConfigs = - mutableListOf() - private val composableConfigs = mutableListOf() - private val setupConfigs = mutableListOf() - - fun with(config: RoborazziComposeConfig): RoborazziComposeConfigBuilder { - if (config is RoborazziComposeActivityScenarioConfig) { - activityScenarioConfigs.add(config) - } - if (config is RoborazziComposeComposableConfig) { - composableConfigs.add(config) +class RoborazziComposeOptions private constructor( + private val activityScenarioOptions: List, + private val composableOptions: List, + private val setupOptions: List +) { + class Builder { + private val activityScenarioOptions = + mutableListOf() + private val composableOptions = mutableListOf() + private val setupOptions = mutableListOf() + + fun addOption(option: RoborazziComposeOption): Builder { + if (option is RoborazziComposeActivityScenarioOption) { + activityScenarioOptions.add(option) + } + if (option is RoborazziComposeComposableOption) { + composableOptions.add(option) + } + if (option is RoborazziComposeSetupOption) { + setupOptions.add(option) + } + return this } - if (config is RoborazziComposeSetupConfig) { - setupConfigs.add(config) + + fun build(): RoborazziComposeOptions { + return RoborazziComposeOptions( + activityScenarioOptions = activityScenarioOptions, + composableOptions = composableOptions, + setupOptions = setupOptions + ) } - return this + } + + fun builder(): Builder { + return Builder() + .apply { + activityScenarioOptions.forEach { addOption(it) } + composableOptions.forEach { addOption(it) } + setupOptions.forEach { addOption(it) } + } } @ExperimentalRoborazziApi - fun configure( - scenario: ActivityScenario, + fun configured( + activityScenario: ActivityScenario, content: @Composable () -> Unit ): @Composable () -> Unit { - setupConfigs.forEach { it.configure() } - activityScenarioConfigs.forEach { it.configureWithActivityScenario(scenario) } + setupOptions.forEach { it.configure() } + activityScenarioOptions.forEach { it.configureWithActivityScenario(activityScenario) } var appliedContent = content - composableConfigs.forEach { config -> + composableOptions.forEach { config -> appliedContent = config.configureWithComposable(appliedContent) } return { appliedContent() } } + + companion object { + operator fun invoke(block: Builder.() -> Unit = {}): RoborazziComposeOptions { + return Builder().apply(block).build() + } + } } @ExperimentalRoborazziApi -fun RoborazziComposeConfigBuilder.size( +fun RoborazziComposeOptions.Builder.size( widthDp: Int = 0, heightDp: Int = 0 -): RoborazziComposeConfigBuilder { - return with(RoborazziComposeSizeConfig(widthDp, heightDp)) +): RoborazziComposeOptions.Builder { + return addOption(RoborazziComposeSizeOption(widthDp, heightDp)) } @ExperimentalRoborazziApi -data class RoborazziComposeSizeConfig(val widthDp: Int, val heightDp: Int) : - RoborazziComposeActivityScenarioConfig, - RoborazziComposeComposableConfig { +data class RoborazziComposeSizeOption(val widthDp: Int, val heightDp: Int) : + RoborazziComposeActivityScenarioOption, + RoborazziComposeComposableOption { override fun configureWithActivityScenario(scenario: ActivityScenario) { scenario.onActivity { activity -> activity.setDisplaySize(widthDp = widthDp, heightDp = heightDp) @@ -130,18 +159,18 @@ data class RoborazziComposeSizeConfig(val widthDp: Int, val heightDp: Int) : } @ExperimentalRoborazziApi -fun RoborazziComposeConfigBuilder.background( +fun RoborazziComposeOptions.Builder.background( showBackground: Boolean, backgroundColor: Long = 0L -): RoborazziComposeConfigBuilder { - return with(RoborazziComposeBackgroundConfig(showBackground, backgroundColor)) +): RoborazziComposeOptions.Builder { + return addOption(RoborazziComposeBackgroundOption(showBackground, backgroundColor)) } @ExperimentalRoborazziApi -data class RoborazziComposeBackgroundConfig( +data class RoborazziComposeBackgroundOption( private val showBackground: Boolean, private val backgroundColor: Long -) : RoborazziComposeActivityScenarioConfig { +) : RoborazziComposeActivityScenarioOption { override fun configureWithActivityScenario(scenario: ActivityScenario) { when (showBackground) { false -> { @@ -164,13 +193,13 @@ data class RoborazziComposeBackgroundConfig( } @ExperimentalRoborazziApi -fun RoborazziComposeConfigBuilder.uiMode(uiMode: Int): RoborazziComposeConfigBuilder { - return with(RoborazziComposeUiModeConfig(uiMode)) +fun RoborazziComposeOptions.Builder.uiMode(uiMode: Int): RoborazziComposeOptions.Builder { + return addOption(RoborazziComposeUiModeOption(uiMode)) } @ExperimentalRoborazziApi -data class RoborazziComposeUiModeConfig(private val uiMode: Int) : - RoborazziComposeSetupConfig { +data class RoborazziComposeUiModeOption(private val uiMode: Int) : + RoborazziComposeSetupOption { override fun configure() { val nightMode = when (uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) { @@ -182,13 +211,13 @@ data class RoborazziComposeUiModeConfig(private val uiMode: Int) : } @ExperimentalRoborazziApi -fun RoborazziComposeConfigBuilder.locale(locale: String): RoborazziComposeConfigBuilder { - return with(RoborazziComposeLocaleConfig(locale)) +fun RoborazziComposeOptions.Builder.locale(locale: String): RoborazziComposeOptions.Builder { + return addOption(RoborazziComposeLocaleOption(locale)) } @ExperimentalRoborazziApi -data class RoborazziComposeLocaleConfig(private val locale: String) : - RoborazziComposeSetupConfig { +data class RoborazziComposeLocaleOption(private val locale: String) : + RoborazziComposeSetupOption { override fun configure() { val localeWithFallback = locale.ifBlank { "en" } setQualifiers("+$localeWithFallback") @@ -196,16 +225,17 @@ data class RoborazziComposeLocaleConfig(private val locale: String) : } @ExperimentalRoborazziApi -fun RoborazziComposeConfigBuilder.fontScale(fontScale: Float): RoborazziComposeConfigBuilder { - return with(RoborazziComposeFontScaleConfig(fontScale)) +fun RoborazziComposeOptions.Builder.fontScale(fontScale: Float): RoborazziComposeOptions.Builder { + return addOption(RoborazziComposeFontScaleOption(fontScale)) } @ExperimentalRoborazziApi -data class RoborazziComposeFontScaleConfig(private val fontScale: Float) : - RoborazziComposeSetupConfig { +data class RoborazziComposeFontScaleOption(private val fontScale: Float) : + RoborazziComposeSetupOption { init { require(fontScale > 0) { "fontScale must be greater than 0" } } + override fun configure() { setFontScale(fontScale) } diff --git a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ManualTest.kt b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ManualTest.kt index 7a99302b..83c2092a 100644 --- a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ManualTest.kt +++ b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ManualTest.kt @@ -1,16 +1,24 @@ package com.github.takahirom.roborazzi.sample +import android.app.Activity import android.graphics.Bitmap import android.graphics.Color import android.view.View import android.widget.TextView +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onParent import androidx.compose.ui.test.performClick +import androidx.compose.ui.unit.dp import androidx.core.graphics.applyCanvas import androidx.core.graphics.createBitmap +import androidx.test.core.app.ActivityScenario import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.pressBack import androidx.test.espresso.action.ViewActions.click @@ -23,11 +31,15 @@ import com.github.takahirom.roborazzi.Dump import com.github.takahirom.roborazzi.ExperimentalRoborazziApi import com.github.takahirom.roborazzi.RoboComponent import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers +import com.github.takahirom.roborazzi.RoborazziComposeActivityScenarioOption +import com.github.takahirom.roborazzi.RoborazziComposeComposableOption +import com.github.takahirom.roborazzi.RoborazziComposeOptions import com.github.takahirom.roborazzi.RoborazziOptions import com.github.takahirom.roborazzi.captureRoboAllImage import com.github.takahirom.roborazzi.captureRoboGif import com.github.takahirom.roborazzi.captureRoboImage import com.github.takahirom.roborazzi.captureRoboLastImage +import com.github.takahirom.roborazzi.fontScale import com.github.takahirom.roborazzi.roboOutputName import com.github.takahirom.roborazzi.roborazziSystemPropertyOutputDirectory import com.github.takahirom.roborazzi.withComposeTestTag @@ -157,6 +169,43 @@ class ManualTest { } } + @OptIn(ExperimentalRoborazziApi::class) + @Test + fun captureComposeLambdaImageWithRoborazziComposeOptions() { + captureRoboImage( + "${roborazziSystemPropertyOutputDirectory()}/manual_compose_with_compose_options.png", + roborazziComposeOptions = RoborazziComposeOptions { + // We have several options to configure the test environment. + fontScale(2f) + // We can also configure the activity scenario and the composable content. + addOption( + object : RoborazziComposeComposableOption, + RoborazziComposeActivityScenarioOption { + override fun configureWithActivityScenario(scenario: ActivityScenario) { + scenario.onActivity { + it.window.decorView.setBackgroundColor(Color.BLUE) + } + } + + override fun configureWithComposable(content: @Composable () -> Unit): @Composable () -> Unit { + return { + Box(Modifier + .padding(10.dp) + .background(color = androidx.compose.ui.graphics.Color.Red) + .padding(10.dp) + ) { + content() + } + } + } + } + ) + }, + ) { + Text("Hello Compose!") + } + } + @OptIn(ExperimentalRoborazziApi::class) @Test fun captureBitmapImage() {