Skip to content

Commit

Permalink
Merge branch 'main' into takahirom/add-workaround-for-overlap/2024-12-12
Browse files Browse the repository at this point in the history
  • Loading branch information
takahirom authored Dec 15, 2024
2 parents afe80c0 + f508092 commit 7382573
Show file tree
Hide file tree
Showing 15 changed files with 258 additions and 111 deletions.
4 changes: 2 additions & 2 deletions gradle/android.gradle
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
android {
compileSdk 35
compileSdk 34

defaultConfig {
minSdk 21
targetSdk 35
targetSdk 32
versionCode 1
versionName "1.0"

Expand Down
4 changes: 2 additions & 2 deletions include-build/roborazzi-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,11 @@ kotlin {

android {
namespace 'com.github.takahirom.roborazzi.core'
compileSdk 35
compileSdk 34

defaultConfig {
minSdk 21
targetSdk 35
targetSdk 32

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
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}")
}
}
4 changes: 2 additions & 2 deletions roborazzi-accessibility-check/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ if (System.getenv("INTEGRATION_TEST") != "true") {

android {
namespace 'com.github.takahirom.roborazzi.accessibility.check'
compileSdk 35
compileSdk 34

defaultConfig {
minSdk 21
targetSdk 35
targetSdk 32

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
4 changes: 2 additions & 2 deletions roborazzi-ai-gemini/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ kotlin {

android {
namespace 'com.github.takahirom.roborazzi.ai.gemini'
compileSdk 35
compileSdk 34

defaultConfig {
minSdk 21
targetSdk 35
targetSdk 32

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
4 changes: 2 additions & 2 deletions roborazzi-ai-openai/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ kotlin {

android {
namespace 'com.github.takahirom.roborazzi.ai.openai'
compileSdk 35
compileSdk 34

defaultConfig {
minSdk 21
targetSdk 35
targetSdk 32

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
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,5 +1,6 @@
package com.github.takahirom.roborazzi

import android.annotation.SuppressLint
import android.view.ViewGroup
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
Expand Down Expand Up @@ -101,12 +102,17 @@ 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)
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)
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ 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.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.unit.dp
import androidx.test.core.app.ActivityScenario
import org.robolectric.RuntimeEnvironment.setFontScale
Expand Down Expand Up @@ -239,4 +241,23 @@ data class RoborazziComposeFontScaleOption(private val fontScale: Float) :
override fun configure() {
setFontScale(fontScale)
}
}
}

@ExperimentalRoborazziApi
fun RoborazziComposeOptions.Builder.localInspectionMode(
localInspectionMode: Boolean
): RoborazziComposeOptions.Builder =
addOption(RoborazziComposeLocalInspectionModeOption(localInspectionMode))


@ExperimentalRoborazziApi
data class RoborazziComposeLocalInspectionModeOption(private val localInspectionMode: Boolean) :
RoborazziComposeComposableOption {
override fun configureWithComposable(
content: @Composable () -> Unit
): @Composable () -> Unit = {
CompositionLocalProvider(LocalInspectionMode provides localInspectionMode) {
content()
}
}
}
4 changes: 2 additions & 2 deletions roborazzi-junit-rule/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ if (System.getenv("INTEGRATION_TEST") != "true") {

android {
namespace 'com.github.takahirom.roborazzi.junit.rule'
compileSdk 35
compileSdk 34

defaultConfig {
minSdk 21
targetSdk 35
targetSdk 32

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
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(
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> {
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
@@ -0,0 +1,89 @@
package com.github.takahirom.roborazzi.sample

import android.app.Activity
import android.graphics.Color
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.unit.dp
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.takahirom.roborazzi.ExperimentalRoborazziApi
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.captureRoboImage
import com.github.takahirom.roborazzi.fontScale
import com.github.takahirom.roborazzi.roborazziSystemPropertyOutputDirectory
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode

@RunWith(AndroidJUnit4::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(
sdk = [30],
qualifiers = RobolectricDeviceQualifiers.NexusOne
)
class ComposeLambdaTest {
@OptIn(ExperimentalRoborazziApi::class)
@Test
fun captureComposeLambdaImage() {
captureRoboImage("${roborazziSystemPropertyOutputDirectory()}/manual_compose.png") {
Text("Hello Compose!")
}
}

@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 don't specify `localInspectionMode` by default.
The default value for `localInspectionMode` in Compose is `false`.
This is to maintain higher fidelity in tests.
If you encounter issues integrating the library, you can set `localInspectionMode` to `true`.
localInspectionMode(true)
*/

// We can also configure the activity scenario and the composable content.
addOption(
object : RoborazziComposeComposableOption,
RoborazziComposeActivityScenarioOption {
override fun configureWithActivityScenario(scenario: ActivityScenario<out Activity>) {
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!")
}
}
}
Loading

0 comments on commit 7382573

Please sign in to comment.