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

Fix alert dialog support #555

Merged
merged 10 commits into from
Dec 15, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ class CustomPreviewTester : ComposePreviewTester<AndroidPreviewInfo> by AndroidC
composeTestRule.setContent {
preview()
}
composeTestRule.onRoot().captureRoboImage("${roborazziSystemPropertyOutputDirectory()}/${preview.methodName}.png")
composeTestRule.onRoot().captureRoboImage("${roborazziSystemPropertyOutputDirectory()}/${preview.methodName}.${provideRoborazziContext().imageExtension}")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ class AndroidComposePreviewTester : ComposePreviewTester<AndroidPreviewInfo> {
preview.declaringClass,
createScreenshotIdFor(preview)
)
val filePath = "$pathPrefix$name.png"
val filePath = "$pathPrefix$name.${provideRoborazziContext().imageExtension}"
preview.captureRoboImage(filePath)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.github.takahirom.roborazzi

import android.annotation.SuppressLint
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.espresso.Espresso
import java.io.File


Expand Down Expand Up @@ -102,11 +104,19 @@ private fun ActivityScenario<out ComponentActivity>.captureRoboImage(
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)
// Views needs to be laid out before we can capture them
Espresso.onIdle()
captureScreenIfMultipleWindows(
file = file,
roborazziOptions = roborazziOptions,
captureSingleComponent = {
val composeView = activity.window.decorView
.findViewById<ViewGroup>(android.R.id.content)
.getChildAt(0) as ComposeView
@SuppressLint("VisibleForTests")
val viewRootForTest = composeView.getChildAt(0) as ViewRootForTest
viewRootForTest.view.captureRoboImage(file, roborazziOptions)
}
)
}
}
87 changes: 67 additions & 20 deletions roborazzi/src/main/java/com/github/takahirom/roborazzi/Roborazzi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -144,18 +144,22 @@ fun captureScreenRoboImage(
// Views needs to be laid out before we can capture them
Espresso.onIdle()

val rootsOracle = RootsOracle_Factory({ Looper.getMainLooper() })
.get()
// Invoke rootOracle.listActiveRoots() via reflection
val listActiveRoots = rootsOracle.javaClass.getMethod("listActiveRoots")
listActiveRoots.isAccessible = true
@Suppress("UNCHECKED_CAST") val roots: List<Root> = listActiveRoots.invoke(rootsOracle) as List<Root>
val roots: List<Root> = fetchRobolectricWindowRoots()
debugLog {
"captureScreenRoboImage roots: ${roots.joinToString("\n") { it.toString() }}"
}
captureRootsInternal(roots, roborazziOptions, file)
}

@InternalRoborazziApi
fun captureRootsInternal(
roots: List<Root>,
roborazziOptions: RoborazziOptions,
file: File
) {
capture(
rootComponent = RoboComponent.Screen(
rootsOrderByDepth = roots.sortedBy { it.windowLayoutParams.get()?.type },
rootsOrderByDepth = roots,
roborazziOptions = roborazziOptions
),
roborazziOptions = roborazziOptions,
Expand All @@ -169,6 +173,43 @@ fun captureScreenRoboImage(
}
}

@InternalRoborazziApi
fun captureScreenIfMultipleWindows(
Copy link
Owner Author

Choose a reason for hiding this comment

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

@sergio-sastre
Thanks as always. I'm thinking about adding support for the AlertDialog() Composable.
Currently, it throws an IllegalStateException, and I would like to fix that behavior.

The basic strategy here is to use captureScreenRoboImage() (which merges all windows into one screenshot) if there are multiple windows.
Do you have any thought on this?
image

Copy link
Contributor

@sergio-sastre sergio-sastre Dec 15, 2024

Choose a reason for hiding this comment

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

@takahirom
Oh sorry, I‘ve just seen this and also saw that it was merged.
I think it is fine as first solution.

Getting it right with the windows is a tricky one.

Have you tried by converting the corresponding ComposeView to bitmap and capturing a bitmap from it? That would likely avoid the IllegalStateException. However, I am not sure whether we could use PixelCopy to draw the bitmap with elevation.

what I mean is to use sth like this composeView.drawToBitmap().captureRoboImage()

maybe worth giving it a try?

file: File,
roborazziOptions: RoborazziOptions,
captureSingleComponent: () -> Unit
) {
if (fetchRobolectricWindowRoots().size > 1) {
roborazziReportLog(
"It seems that there are multiple windows." +
"We capture all windows using captureScreenRoboImage(). " +
"We can add a flag to disable this behavior so please let us know if you need it."
)
captureScreenRoboImage(file, roborazziOptions)
} else {
captureSingleComponent()
}
}

@InternalRoborazziApi
fun fetchRobolectricWindowRoots(): List<Root> {
Copy link
Owner Author

@takahirom takahirom Dec 14, 2024

Choose a reason for hiding this comment

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

This is the method we use for captureScreenRoboImage() to fetch windows.

try {
@Suppress("INACCESSIBLE_TYPE") val rootsOracle = RootsOracle_Factory({ Looper.getMainLooper() })
.get()
// Invoke rootOracle.listActiveRoots() via reflection
@Suppress("INACCESSIBLE_TYPE") val listActiveRoots =
rootsOracle.javaClass.getMethod("listActiveRoots")
listActiveRoots.isAccessible = true
@Suppress("UNCHECKED_CAST", "INACCESSIBLE_TYPE") val roots: List<Root> =
(listActiveRoots.invoke(rootsOracle) as List<Root>
).sortedBy { it.windowLayoutParams.get()?.type }
return roots
} catch (e: Throwable) {
e.printStackTrace()
return emptyList()
}
}

fun Bitmap.captureRoboImage(
filePath: String = DefaultFileNameGenerator.generateFilePath(),
roborazziOptions: RoborazziOptions = provideRoborazziContext().options,
Expand Down Expand Up @@ -278,20 +319,26 @@ fun SemanticsNodeInteraction.captureRoboImage(
roborazziOptions: RoborazziOptions = provideRoborazziContext().options,
) {
if (!roborazziOptions.taskType.isEnabled()) return
capture(
rootComponent = RoboComponent.Compose(
node = this.fetchSemanticsNode("fail to captureRoboImage"),
roborazziOptions = roborazziOptions
),
captureScreenIfMultipleWindows(
file = file,
roborazziOptions = roborazziOptions,
) { canvas ->
processOutputImageAndReportWithDefaults(
canvas = canvas,
goldenFile = file,
roborazziOptions = roborazziOptions
)
canvas.release()
}
captureSingleComponent = {
capture(
rootComponent = RoboComponent.Compose(
node = this.fetchSemanticsNode("fail to captureRoboImage"),
roborazziOptions = roborazziOptions
),
roborazziOptions = roborazziOptions,
) { canvas ->
processOutputImageAndReportWithDefaults(
canvas = canvas,
goldenFile = file,
roborazziOptions = roborazziOptions
)
canvas.release()
}
}
)
}

fun SemanticsNodeInteraction.captureRoboGif(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package com.github.takahirom.preview.tests

import android.content.res.Configuration
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
Expand Down Expand Up @@ -111,6 +114,36 @@ fun PreviewWithProperties2() {
}
}

@Preview
@Composable
fun PreviewDialog() {
MaterialTheme {
AlertDialog(
onDismissRequest = {},
confirmButton = @Composable { Text("Confirm") },
text = @Composable { Text("Generate Preview Test Sample!") }
)
}
}

@Preview
@Composable
fun PreviewDialogSurface() {
MaterialTheme {
Surface {
Box(Modifier.height(300.dp)) {
Text("Hello, World!")
}
AlertDialog(
onDismissRequest = {},
confirmButton = @Composable { Text("Confirm") },
text = @Composable { Text("Generate Preview Test Sample!") }
)
}
}
}


@Preview(
name = "Preview width & height large",
widthDp = 2000,
Expand Down Expand Up @@ -212,4 +245,4 @@ fun PreviewShowBackgroundWithBackgroundColor() {
text = "Hello, World!"
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package com.github.takahirom.sample
package com.github.takahirom

import org.junit.Assert.assertEquals
import org.junit.Test

import org.junit.Assert.*

/**
* Example local unit test, which will execute on the development machine (host).
*
Expand Down
Loading