From fa0a375300090f7cb5ee12bfbce8311970248dc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Sastre=20Fl=C3=B3rez?= Date: Sun, 24 Nov 2024 09:54:49 +0100 Subject: [PATCH 01/10] Add support for heightDp, widthDp, showBackground, backgroundColor --- .../RoborazziPreviewScannerSupport.kt | 9 +- roborazzi-compose/build.gradle | 1 + .../takahirom/roborazzi/RoborazziCompose.kt | 18 ++- .../roborazzi/RoborazziComposePreview.kt | 123 ++++++++++++++++++ 4 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposePreview.kt 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 14ed93a80..5174a0273 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 @@ -14,7 +14,14 @@ fun ComposablePreview.captureRoboImage( ) { val composablePreview = this composablePreview.applyToRobolectricConfiguration() - captureRoboImage(filePath = filePath, roborazziOptions = roborazziOptions) { + captureSizedRoboImage( + filePath = filePath, + roborazziOptions = roborazziOptions, + widthDp = composablePreview.previewInfo.widthDp, + heightDp = composablePreview.previewInfo.heightDp, + showBackground = composablePreview.previewInfo.showBackground, + backgroundColor = composablePreview.previewInfo.backgroundColor + ) { composablePreview() } } diff --git a/roborazzi-compose/build.gradle b/roborazzi-compose/build.gradle index e829d9787..b7bfced34 100644 --- a/roborazzi-compose/build.gradle +++ b/roborazzi-compose/build.gradle @@ -17,6 +17,7 @@ dependencies { testImplementation libs.androidx.compose.runtime compileOnly libs.androidx.compose.ui + compileOnly libs.androidx.compose.foundation compileOnly libs.androidx.activity.compose compileOnly libs.robolectric } \ No newline at end of file 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 7c024de03..a23ad6b4b 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 @@ -1,16 +1,26 @@ package com.github.takahirom.roborazzi +import android.app.Activity import android.app.Application import android.content.ComponentName import android.view.ViewGroup +import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewRootForTest +import androidx.compose.ui.unit.dp import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider import org.robolectric.Shadows +import org.robolectric.shadows.ShadowDisplay import java.io.File +import kotlin.math.roundToInt fun captureRoboImage( filePath: String = DefaultFileNameGenerator.generateFilePath(), @@ -30,7 +40,7 @@ fun captureRoboImage( content: @Composable () -> Unit, ) { if (!roborazziOptions.taskType.isEnabled()) return - registerRoborazziActivityToRobolectricIfNeeded() + registerRoborazziActivityToRobolectricIfNeeded(RoborazziTransparentActivity::class.java) val activityScenario = ActivityScenario.launch(RoborazziTransparentActivity::class.java) activityScenario.use { activityScenario.onActivity { activity -> @@ -51,13 +61,15 @@ fun captureRoboImage( /** * Workaround for https://github.com/takahirom/roborazzi/issues/100 */ -private fun registerRoborazziActivityToRobolectricIfNeeded() { +internal fun registerRoborazziActivityToRobolectricIfNeeded( + activityClass: Class +) { try { val appContext: Application = ApplicationProvider.getApplicationContext() Shadows.shadowOf(appContext.packageManager).addActivityIfNotPresent( ComponentName( appContext.packageName, - RoborazziTransparentActivity::class.java.name, + activityClass::class.java.name, ) ) } catch (e: ClassNotFoundException) { diff --git a/roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposePreview.kt b/roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposePreview.kt new file mode 100644 index 000000000..97fd69497 --- /dev/null +++ b/roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposePreview.kt @@ -0,0 +1,123 @@ +package com.github.takahirom.roborazzi + +import android.app.Activity +import android.graphics.Color +import android.view.ViewGroup +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewRootForTest +import androidx.compose.ui.unit.dp +import androidx.test.core.app.ActivityScenario +import org.robolectric.Shadows +import org.robolectric.shadows.ShadowDisplay +import java.io.File +import kotlin.math.roundToInt + +fun captureSizedRoboImage( + widthDp: Int, + heightDp: Int, + showBackground: Boolean, + backgroundColor: Long, + filePath: String, + roborazziOptions: RoborazziOptions = provideRoborazziContext().options, + content: @Composable () -> Unit, +) { + if (!roborazziOptions.taskType.isEnabled()) return + registerRoborazziActivityToRobolectricIfNeeded(ComponentActivity::class.java) + + val activityScenario = ActivityScenario.launch(ComponentActivity::class.java) + + activityScenario.onActivity { + it.setShadowDisplay( + widthDp = widthDp, + heightDp = heightDp + ) + } + + activityScenario.use { + activityScenario.onActivity { activity -> + + activity.setBackgroundColor( + showBackground = showBackground, + backgroundColor = backgroundColor + ) + + activity.setContent( + content = content.withSize(widthDp = widthDp, heightDp = heightDp) + ) + + val composeView = activity.window.decorView + .findViewById(android.R.id.content) + .getChildAt(0) as ComposeView + + val viewRootForTest = composeView.getChildAt(0) as ViewRootForTest + viewRootForTest.view.captureRoboImage(filePath, roborazziOptions) + } + + // Closing the activity is necessary to prevent memory leaks. + // If multiple captureRoboImage calls occur in a single test, + // they can lead to an activity leak. + } +} + +fun Activity.setBackgroundColor( + showBackground: Boolean, + backgroundColor: Long, +) { + when (showBackground) { + false -> window.decorView.setBackgroundColor(Color.TRANSPARENT) + true -> if (backgroundColor != 0L) { + window.decorView.setBackgroundColor(backgroundColor.toInt()) + } + } +} + +fun Activity.setShadowDisplay( + widthDp: Int, + heightDp: Int +) { + if (widthDp > 0 || heightDp > 0) { + val display = ShadowDisplay.getDefaultDisplay() + val density = resources.displayMetrics.density + if (widthDp > 0) { + widthDp.let { + val widthPx = (widthDp * density).roundToInt() + Shadows.shadowOf(display).setWidth(widthPx) + } + } + if (heightDp > 0) { + heightDp.let { + val heightPx = (heightDp * density).roundToInt() + Shadows.shadowOf(display).setHeight(heightPx) + } + } + recreate() + } +} + +/** + * WARNING: + * For this to work, it requires that the Display is within the widthDp and heightDp dimensions + */ +private fun (@Composable () -> Unit).withSize( + widthDp: Int, + heightDp: Int, +): @Composable () -> Unit { + val resizedPreview = @Composable { + val modifier = when { + widthDp > 0 && heightDp > 0 -> Modifier.size(widthDp.dp, heightDp.dp) + widthDp > 0 -> Modifier.width(widthDp.dp) + heightDp > 0 -> Modifier.height(heightDp.dp) + else -> Modifier + } + Box(modifier = modifier) { this@withSize() } + } + return resizedPreview +} \ No newline at end of file From 9b5068dd30cb1b1319cd17900d024696a35804ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Sastre=20Fl=C3=B3rez?= Date: Sun, 24 Nov 2024 19:07:50 +0100 Subject: [PATCH 02/10] fix build so it takes screenshots --- .../takahirom/roborazzi/RoborazziCompose.kt | 18 +--- .../roborazzi/RoborazziComposePreview.kt | 36 ++++++-- roborazzi/src/main/AndroidManifest.xml | 1 + .../RoborazziComposePreviewsActivity.kt | 11 +++ .../takahirom/preview/tests/Previews.kt | 85 +++++++++++++++++-- 5 files changed, 124 insertions(+), 27 deletions(-) create mode 100644 roborazzi/src/main/java/com/github/takahirom/roborazzi/RoborazziComposePreviewsActivity.kt 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 a23ad6b4b..4a2ac142f 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 @@ -1,26 +1,16 @@ package com.github.takahirom.roborazzi -import android.app.Activity import android.app.Application import android.content.ComponentName import android.view.ViewGroup -import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewRootForTest -import androidx.compose.ui.unit.dp import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider import org.robolectric.Shadows -import org.robolectric.shadows.ShadowDisplay import java.io.File -import kotlin.math.roundToInt fun captureRoboImage( filePath: String = DefaultFileNameGenerator.generateFilePath(), @@ -40,7 +30,7 @@ fun captureRoboImage( content: @Composable () -> Unit, ) { if (!roborazziOptions.taskType.isEnabled()) return - registerRoborazziActivityToRobolectricIfNeeded(RoborazziTransparentActivity::class.java) + registerRoborazziTransparentActivityToRobolectricIfNeeded() val activityScenario = ActivityScenario.launch(RoborazziTransparentActivity::class.java) activityScenario.use { activityScenario.onActivity { activity -> @@ -61,15 +51,13 @@ fun captureRoboImage( /** * Workaround for https://github.com/takahirom/roborazzi/issues/100 */ -internal fun registerRoborazziActivityToRobolectricIfNeeded( - activityClass: Class -) { +private fun registerRoborazziTransparentActivityToRobolectricIfNeeded() { try { val appContext: Application = ApplicationProvider.getApplicationContext() Shadows.shadowOf(appContext.packageManager).addActivityIfNotPresent( ComponentName( appContext.packageName, - activityClass::class.java.name, + RoborazziTransparentActivity::class.java.name, ) ) } catch (e: ClassNotFoundException) { diff --git a/roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposePreview.kt b/roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposePreview.kt index 97fd69497..b77352ae5 100644 --- a/roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposePreview.kt +++ b/roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposePreview.kt @@ -1,12 +1,18 @@ package com.github.takahirom.roborazzi import android.app.Activity +import android.app.Application +import android.app.Instrumentation +import android.content.ComponentName import android.graphics.Color import android.view.ViewGroup import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable @@ -15,9 +21,9 @@ import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewRootForTest import androidx.compose.ui.unit.dp import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider import org.robolectric.Shadows import org.robolectric.shadows.ShadowDisplay -import java.io.File import kotlin.math.roundToInt fun captureSizedRoboImage( @@ -30,12 +36,13 @@ fun captureSizedRoboImage( content: @Composable () -> Unit, ) { if (!roborazziOptions.taskType.isEnabled()) return - registerRoborazziActivityToRobolectricIfNeeded(ComponentActivity::class.java) + registerRoborazziComposePreviewsActivityToRobolectricIfNeeded() val activityScenario = ActivityScenario.launch(ComponentActivity::class.java) - activityScenario.onActivity { - it.setShadowDisplay( + activityScenario.onActivity { activity -> + + activity.setDisplaySize( widthDp = widthDp, heightDp = heightDp ) @@ -79,7 +86,7 @@ fun Activity.setBackgroundColor( } } -fun Activity.setShadowDisplay( +fun Activity.setDisplaySize( widthDp: Int, heightDp: Int ) { @@ -117,7 +124,24 @@ private fun (@Composable () -> Unit).withSize( heightDp > 0 -> Modifier.height(heightDp.dp) else -> Modifier } - Box(modifier = modifier) { this@withSize() } + Box(modifier = modifier) { + this@withSize() + } } return resizedPreview +} + +private fun registerRoborazziComposePreviewsActivityToRobolectricIfNeeded() { + try { + val appContext: Application = ApplicationProvider.getApplicationContext() + Shadows.shadowOf(appContext.packageManager).addActivityIfNotPresent( + ComponentName( + appContext.packageName, + ComponentActivity::class.java.name, + ) + ) + } catch (e: ClassNotFoundException) { + // Configured to run even without Robolectric + e.printStackTrace() + } } \ No newline at end of file diff --git a/roborazzi/src/main/AndroidManifest.xml b/roborazzi/src/main/AndroidManifest.xml index f1c33faff..d090f40ed 100644 --- a/roborazzi/src/main/AndroidManifest.xml +++ b/roborazzi/src/main/AndroidManifest.xml @@ -3,5 +3,6 @@ + \ No newline at end of file diff --git a/roborazzi/src/main/java/com/github/takahirom/roborazzi/RoborazziComposePreviewsActivity.kt b/roborazzi/src/main/java/com/github/takahirom/roborazzi/RoborazziComposePreviewsActivity.kt new file mode 100644 index 000000000..a943dad1b --- /dev/null +++ b/roborazzi/src/main/java/com/github/takahirom/roborazzi/RoborazziComposePreviewsActivity.kt @@ -0,0 +1,11 @@ +package com.github.takahirom.roborazzi + +import android.os.Bundle +import android.os.PersistableBundle +import androidx.activity.ComponentActivity + +class RoborazziComposePreviewsActivity: ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { + super.onCreate(savedInstanceState, persistentState) + } +} \ No newline at end of file diff --git a/sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt b/sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt index 233c868cf..606e27848 100644 --- a/sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt +++ b/sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt @@ -2,6 +2,8 @@ package com.github.takahirom.preview.tests import android.content.res.Configuration import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.Card @@ -59,19 +61,18 @@ fun PreviewDarkMode() { @Preview( name = "Preview Name", - // These properties are not supported by Roborazzi yet. group = "Preview Group", - apiLevel = 30, - widthDp = 320, - heightDp = 640, locale = "ja-rJP", fontScale = 1.5f, + widthDp = 320, + heightDp = 640, + // These properties are not supported by Roborazzi yet. + apiLevel = 30 ) @Composable fun PreviewWithProperties1() { Card( - Modifier - .width(100.dp) + Modifier.width(100.dp) ) { Text( modifier = Modifier.padding(8.dp), @@ -99,4 +100,76 @@ fun PreviewWithProperties2() { text = "Hello, World!" ) } +} + +@Preview( + name = "Preview width & height large", + group = "Preview Group", + widthDp = 2000, + heightDp = 1000, +) +@Composable +fun PreviewWithWidthAndHeight() { + Card( + Modifier.fillMaxSize() + ) { + Text( + modifier = Modifier.padding(8.dp), + text = "Hello, World!" + ) + } +} + +@Preview( + name = "Preview width & height", + group = "Preview Group", + widthDp = 30, + heightDp = 30, +) +@Composable +fun PreviewWithWidthAndHeightSmall() { + Card( + Modifier.fillMaxSize() + ) { + Text( + modifier = Modifier.padding(8.dp), + text = "Hello, World!" + ) + } +} + +@Preview( + name = "Preview width", + group = "Preview Group", + widthDp = 500, + // These properties are not supported by Roborazzi yet. + apiLevel = 30 +) +@Composable +fun PreviewWithWidth() { + Card( + Modifier.fillMaxSize() + ) { + Text( + modifier = Modifier.padding(8.dp), + text = "Hello, World!" + ) + } +} + +@Preview( + name = "Preview height", + group = "Preview Group", + heightDp = 500, +) +@Composable +fun PreviewWithHeight() { + Card( + Modifier.fillMaxSize() + ) { + Text( + modifier = Modifier.padding(8.dp), + text = "Hello, World!" + ) + } } \ No newline at end of file From e254ec04d133eee2ecbeea7212c161c666a02c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Sastre=20Fl=C3=B3rez?= Date: Mon, 25 Nov 2024 17:21:58 +0100 Subject: [PATCH 03/10] Adjust height to consider ActionBar in setDisplaySize --- .../roborazzi/RoborazziComposePreview.kt | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposePreview.kt b/roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposePreview.kt index b77352ae5..bceb0cc93 100644 --- a/roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposePreview.kt +++ b/roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposePreview.kt @@ -2,17 +2,12 @@ package com.github.takahirom.roborazzi import android.app.Activity import android.app.Application -import android.app.Instrumentation import android.content.ComponentName import android.graphics.Color import android.view.ViewGroup -import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable @@ -36,9 +31,9 @@ fun captureSizedRoboImage( content: @Composable () -> Unit, ) { if (!roborazziOptions.taskType.isEnabled()) return - registerRoborazziComposePreviewsActivityToRobolectricIfNeeded() + registerTransparentActivityToRobolectricIfNeeded() - val activityScenario = ActivityScenario.launch(ComponentActivity::class.java) + val activityScenario = ActivityScenario.launch(RoborazziTransparentActivity::class.java) activityScenario.onActivity { activity -> @@ -100,8 +95,9 @@ fun Activity.setDisplaySize( } } if (heightDp > 0) { - heightDp.let { - val heightPx = (heightDp * density).roundToInt() + val effectiveHeightDp = heightDp + 56 // 56dp is the size of the ActionBar + effectiveHeightDp.let { + val heightPx = (effectiveHeightDp * density).roundToInt() Shadows.shadowOf(display).setHeight(heightPx) } } @@ -131,13 +127,13 @@ private fun (@Composable () -> Unit).withSize( return resizedPreview } -private fun registerRoborazziComposePreviewsActivityToRobolectricIfNeeded() { +private fun registerTransparentActivityToRobolectricIfNeeded() { try { val appContext: Application = ApplicationProvider.getApplicationContext() Shadows.shadowOf(appContext.packageManager).addActivityIfNotPresent( ComponentName( appContext.packageName, - ComponentActivity::class.java.name, + RoborazziTransparentActivity::class.java.name, ) ) } catch (e: ClassNotFoundException) { From b4ff8cfab7e5460607569b24efd15260bdd41e62 Mon Sep 17 00:00:00 2001 From: takahirom Date: Tue, 26 Nov 2024 11:27:03 +0900 Subject: [PATCH 04/10] Fix size issue --- .../takahirom/roborazzi/RoborazziTransparentActivity.kt | 6 +++--- roborazzi/src/main/res/values/themes.xml | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/roborazzi/src/main/java/com/github/takahirom/roborazzi/RoborazziTransparentActivity.kt b/roborazzi/src/main/java/com/github/takahirom/roborazzi/RoborazziTransparentActivity.kt index 05f1d9a8d..599d38c74 100644 --- a/roborazzi/src/main/java/com/github/takahirom/roborazzi/RoborazziTransparentActivity.kt +++ b/roborazzi/src/main/java/com/github/takahirom/roborazzi/RoborazziTransparentActivity.kt @@ -1,11 +1,11 @@ package com.github.takahirom.roborazzi import android.os.Bundle -import android.os.PersistableBundle import androidx.activity.ComponentActivity class RoborazziTransparentActivity: ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { - super.onCreate(savedInstanceState, persistentState) + override fun onCreate(savedInstanceState: Bundle?) { + setTheme(android.R.style.Theme_Translucent_NoTitleBar_Fullscreen) + super.onCreate(savedInstanceState) } } \ No newline at end of file diff --git a/roborazzi/src/main/res/values/themes.xml b/roborazzi/src/main/res/values/themes.xml index 53d159c90..d2b8b6159 100644 --- a/roborazzi/src/main/res/values/themes.xml +++ b/roborazzi/src/main/res/values/themes.xml @@ -1,6 +1,7 @@ +