Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for heightDp, widthDp, showBackground, backgroundColor #576

Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,25 @@ fun ComposablePreview<AndroidPreviewInfo>.captureRoboImage(
) {
val composablePreview = this
composablePreview.applyToRobolectricConfiguration()
captureRoboImage(filePath = filePath, roborazziOptions = roborazziOptions) {
composablePreview()
}
captureRoboImageWithActivityScenarioSetup(
filePath = filePath,
roborazziOptions = roborazziOptions,
content = { activityScenario ->
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it was practical to add such a captureRoboImage method to allow users to set properties through the activityScenario. Open to other opinions

Copy link
Owner

@takahirom takahirom Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. Designing these APIs is a bit challenging. I'm giving a talk at a technology conference this Saturday, so I'm a little busy at the moment. I apologize for the delay, but I’d like to take some time over the weekend to consider this. I’ll get back to you soon.


activityScenario.setBackgroundColor(
showBackground = composablePreview.previewInfo.showBackground,
backgroundColor = composablePreview.previewInfo.backgroundColor
)

activityScenario.createSizedPreview(
widthDp = composablePreview.previewInfo.widthDp,
heightDp = composablePreview.previewInfo.heightDp,
preview = { composablePreview() }
)
},
)
}

/**
* ComposePreviewTester is an interface that allows you to define a custom test for a Composable preview.
* The class that implements this interface should have a parameterless constructor.
Expand Down
1 change: 1 addition & 0 deletions roborazzi-compose/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.github.takahirom.roborazzi

import android.app.Activity
import android.graphics.Color
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.unit.dp
import androidx.test.core.app.ActivityScenario
import org.robolectric.Shadows.shadowOf
import org.robolectric.shadows.ShadowDisplay.getDefaultDisplay
import kotlin.math.roundToInt

fun ActivityScenario<out Activity>.setBackgroundColor(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 Methods together with the captureRoboImageWithActivityScenarioSetup() would allow Roborazzi users to set the background and size of their Composables under tests also for non-preview tests.

maybe worth documenting a bit more about these options to arise awareness?

showBackground: Boolean,
backgroundColor: Long,
) {
when (showBackground) {
false -> {
onActivity { activity ->
activity.window.decorView.setBackgroundColor(Color.TRANSPARENT)
}
}

true -> {
val color = when (backgroundColor != 0L) {
true -> backgroundColor.toInt()
false -> Color.WHITE
}
onActivity { activity ->
activity.window.decorView.setBackgroundColor(color)
}
}
}
}

fun ActivityScenario<out Activity>.createSizedPreview(
widthDp: Int,
heightDp: Int,
preview: @Composable () -> Unit
): @Composable () -> Unit {
var result: (@Composable () -> Unit)? = null
onActivity { activity ->
activity.setDisplaySize(widthDp = widthDp, heightDp = heightDp)
result = preview.size(widthDp = widthDp, heightDp = heightDp)
}
return result
?: throw IllegalStateException("The preview could not be sucessfully sized to widthDp = $widthDp and heightDp = $heightDp")
}


internal fun Activity.setDisplaySize(
widthDp: Int,
heightDp: Int
) {
if (widthDp <= 0 && heightDp <= 0) return
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe desirable to log a warning or debug message if it returns here?


val display = shadowOf(getDefaultDisplay())
val density = resources.displayMetrics.density
if (widthDp > 0) {
val widthPx = (widthDp * density).roundToInt()
display.setWidth(widthPx)
}
if (heightDp > 0) {
val heightPx = (heightDp * density).roundToInt()
display.setHeight(heightPx)
}
recreate()
}

/**
* WARNING:
* For this to work, it requires that the Display is within the widthDp and heightDp dimensions
* You can ensure that by calling [Activity.setDisplaySize] before
*/
internal fun (@Composable () -> Unit).size(
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@size()
}
}
return resizedPreview
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.github.takahirom.roborazzi

import android.app.Application
import android.content.ComponentName
import androidx.test.core.app.ApplicationProvider
import org.robolectric.Shadows

/**
* Workaround for https://github.com/takahirom/roborazzi/issues/100
*/
internal fun registerActivityToRobolectricIfNeeded() {
try {
val appContext: Application = ApplicationProvider.getApplicationContext()
Shadows.shadowOf(appContext.packageManager).addActivityIfNotPresent(
ComponentName(
appContext.packageName,
RoborazziTransparentActivity::class.java.name,
)
)
} catch (e: ClassNotFoundException) {
// Configured to run even without Robolectric
e.printStackTrace()
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package com.github.takahirom.roborazzi

import android.app.Application
import android.content.ComponentName
import android.app.Activity
import android.view.ViewGroup
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewRootForTest
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import org.robolectric.Shadows
import java.io.File

fun captureRoboImage(
Expand All @@ -30,16 +28,12 @@ fun captureRoboImage(
content: @Composable () -> Unit,
) {
if (!roborazziOptions.taskType.isEnabled()) return
registerRoborazziActivityToRobolectricIfNeeded()
registerActivityToRobolectricIfNeeded()

val activityScenario = ActivityScenario.launch(RoborazziTransparentActivity::class.java)
activityScenario.use {
activityScenario.onActivity { activity ->
activity.setContent(content = content)
val composeView = activity.window.decorView
.findViewById<ViewGroup>(android.R.id.content)
.getChildAt(0) as ComposeView
val viewRootForTest = composeView.getChildAt(0) as ViewRootForTest
viewRootForTest.view.captureRoboImage(file, roborazziOptions)
activityScenario.captureRoboImage(file, roborazziOptions){
content()
}

// Closing the activity is necessary to prevent memory leaks.
Expand All @@ -48,20 +42,55 @@ fun captureRoboImage(
}
}

/**
* Workaround for https://github.com/takahirom/roborazzi/issues/100
*/
private fun registerRoborazziActivityToRobolectricIfNeeded() {
try {
val appContext: Application = ApplicationProvider.getApplicationContext()
Shadows.shadowOf(appContext.packageManager).addActivityIfNotPresent(
ComponentName(
appContext.packageName,
RoborazziTransparentActivity::class.java.name,
)
)
} catch (e: ClassNotFoundException) {
// Configured to run even without Robolectric
e.printStackTrace()
fun captureRoboImageWithActivityScenarioSetup(
filePath: String,
roborazziOptions: RoborazziOptions = provideRoborazziContext().options,
content: (ActivityScenario<out Activity>) -> @Composable () -> Unit,
) {
if (!roborazziOptions.taskType.isEnabled()) return
registerActivityToRobolectricIfNeeded()

val activityScenario = ActivityScenario.launch(RoborazziTransparentActivity::class.java)

activityScenario.use {
val sizedPreview = content(activityScenario)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better to name it "preview" instead of "sizedPreview". That is a leftover of the clean up...

activityScenario.captureRoboImage(filePath, roborazziOptions){
sizedPreview()
}

// 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.
}
}


private fun ActivityScenario<out ComponentActivity>.captureRoboImage(
filePath: String,
roborazziOptions: RoborazziOptions = provideRoborazziContext().options,
content: @Composable () -> Unit,
) {
captureRoboImage(
file = fileWithRecordFilePathStrategy(filePath),
roborazziOptions = roborazziOptions,
content = content
)
}

private fun ActivityScenario<out ComponentActivity>.captureRoboImage(
file: File,
roborazziOptions: RoborazziOptions = provideRoborazziContext().options,
content: @Composable () -> Unit,
) {

onActivity { activity ->
activity.setContent(content = { content() })

val composeView = activity.window.decorView
.findViewById<ViewGroup>(android.R.id.content)
.getChildAt(0) as ComposeView

val viewRootForTest = composeView.getChildAt(0) as ViewRootForTest
viewRootForTest.view.captureRoboImage(file, roborazziOptions)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
1 change: 1 addition & 0 deletions roborazzi/src/main/res/values/themes.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<!-- It seems that this theme isn't used because of test resource merger issue -->
<style name="RoborazziTransparentTheme">
<item name="android:colorBackground">@android:color/transparent</item>
<item name="android:windowActionBar">false</item>
Expand Down
2 changes: 2 additions & 0 deletions sample-generate-preview-tests/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ android {
isIncludeAndroidResources = true
all {
it.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware"
// For large preview
it.maxHeapSize = "4096m"
}
}
}
Expand Down
Loading
Loading