From 2c626c4fe5f6f46541bcfde105829654cbf14d5a Mon Sep 17 00:00:00 2001 From: takahirom Date: Fri, 15 Nov 2024 20:11:32 +0900 Subject: [PATCH 1/6] Add alert dialog test --- .../github/takahirom/preview/tests/Previews.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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..2d222eb0f 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 @@ -4,6 +4,7 @@ import android.content.res.Configuration import androidx.compose.foundation.isSystemInDarkTheme 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.MaterialTheme import androidx.compose.material3.Text @@ -99,4 +100,16 @@ fun PreviewWithProperties2() { text = "Hello, World!" ) } -} \ No newline at end of file +} + +@Preview +@Composable +fun PreviewDialog() { + MaterialTheme { + AlertDialog( + onDismissRequest = {}, + confirmButton = @Composable { Text("Confirm") }, + text = @Composable { Text("Generate Preview Test Sample!") } + ) + } +} From 17ef64c8f9e337e724b87c6208ff7467999b96fd Mon Sep 17 00:00:00 2001 From: takahirom Date: Fri, 15 Nov 2024 20:40:57 +0900 Subject: [PATCH 2/6] Fix preview dialog problem(not working yet) --- .../takahirom/sample/CustomPreviewTester.kt | 2 +- .../RoborazziPreviewScannerSupport.kt | 2 +- .../takahirom/roborazzi/RoborazziCompose.kt | 22 +++++++++--- .../github/takahirom/roborazzi/Roborazzi.kt | 36 +++++++++++++++---- .../takahirom/{sample => }/ExampleUnitTest.kt | 5 ++- 5 files changed, 50 insertions(+), 17 deletions(-) rename sample-generate-preview-tests/src/test/java/com/github/takahirom/{sample => }/ExampleUnitTest.kt (81%) diff --git a/include-build/roborazzi-gradle-plugin/src/integrationTest/projects/sample-generate-preview-tests/src/test/java/com/github/takahirom/sample/CustomPreviewTester.kt b/include-build/roborazzi-gradle-plugin/src/integrationTest/projects/sample-generate-preview-tests/src/test/java/com/github/takahirom/sample/CustomPreviewTester.kt index 3f3e6b76a..e122cb0b2 100644 --- a/include-build/roborazzi-gradle-plugin/src/integrationTest/projects/sample-generate-preview-tests/src/test/java/com/github/takahirom/sample/CustomPreviewTester.kt +++ b/include-build/roborazzi-gradle-plugin/src/integrationTest/projects/sample-generate-preview-tests/src/test/java/com/github/takahirom/sample/CustomPreviewTester.kt @@ -34,6 +34,6 @@ class CustomPreviewTester : ComposePreviewTester by AndroidC composeTestRule.setContent { preview() } - composeTestRule.onRoot().captureRoboImage("${roborazziSystemPropertyOutputDirectory()}/${preview.methodName}.png") + composeTestRule.onRoot().captureRoboImage("${roborazziSystemPropertyOutputDirectory()}/${preview.methodName}.${provideRoborazziContext().imageExtension}") } } \ No newline at end of file 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..020bd2845 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 @@ -108,7 +108,7 @@ class AndroidComposePreviewTester : ComposePreviewTester { preview.declaringClass, createScreenshotIdFor(preview) ) - val filePath = "$pathPrefix$name.png" + val filePath = "$pathPrefix$name.${provideRoborazziContext().imageExtension}" preview.captureRoboImage(filePath) } 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..a1a67bb67 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 @@ -9,6 +9,7 @@ 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 androidx.test.espresso.Espresso import org.robolectric.Shadows import java.io.File @@ -31,15 +32,26 @@ fun captureRoboImage( ) { if (!roborazziOptions.taskType.isEnabled()) return registerRoborazziActivityToRobolectricIfNeeded() + // Views needs to be laid out before we can capture them + Espresso.onIdle() + val activityScenario = ActivityScenario.launch(RoborazziTransparentActivity::class.java) activityScenario.use { activityScenario.onActivity { activity -> activity.setContent(content = content) - val composeView = activity.window.decorView - .findViewById(android.R.id.content) - .getChildAt(0) as ComposeView - val viewRootForTest = composeView.getChildAt(0) as ViewRootForTest - viewRootForTest.view.captureRoboImage(file, roborazziOptions) + val windowRoots = fetchRobolectricWindowRoots() + if (windowRoots.size <= 1) { + val composeView = activity.window.decorView + .findViewById(android.R.id.content) + .getChildAt(0) as ComposeView + val viewRootForTest = composeView.getChildAt(0) as ViewRootForTest + viewRootForTest.view.captureRoboImage(file, roborazziOptions) + } else { + // For dialog +// windowRoots[1].decorView.rootView.captureRoboImage(file, roborazziOptions) +// captureScreenRoboImage() + captureRootsInternal(windowRoots.drop(1), roborazziOptions, file) + } } // Closing the activity is necessary to prevent memory leaks. diff --git a/roborazzi/src/main/java/com/github/takahirom/roborazzi/Roborazzi.kt b/roborazzi/src/main/java/com/github/takahirom/roborazzi/Roborazzi.kt index 336b0c126..ffe4d9207 100644 --- a/roborazzi/src/main/java/com/github/takahirom/roborazzi/Roborazzi.kt +++ b/roborazzi/src/main/java/com/github/takahirom/roborazzi/Roborazzi.kt @@ -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 = listActiveRoots.invoke(rootsOracle) as List + val roots: List = fetchRobolectricWindowRoots() debugLog { "captureScreenRoboImage roots: ${roots.joinToString("\n") { it.toString() }}" } + captureRootsInternal(roots, roborazziOptions, file) +} + +@InternalRoborazziApi +fun captureRootsInternal( + roots: List, + roborazziOptions: RoborazziOptions, + file: File +) { capture( rootComponent = RoboComponent.Screen( - rootsOrderByDepth = roots.sortedBy { it.windowLayoutParams.get()?.type }, + rootsOrderByDepth = roots, roborazziOptions = roborazziOptions ), roborazziOptions = roborazziOptions, @@ -169,6 +173,24 @@ fun captureScreenRoboImage( } } +@InternalRoborazziApi +fun fetchRobolectricWindowRoots(): List { + try { + @Suppress("INACCESSIBLE_TYPE") 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 = + (listActiveRoots.invoke(rootsOracle) as List + ).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, diff --git a/sample-generate-preview-tests/src/test/java/com/github/takahirom/sample/ExampleUnitTest.kt b/sample-generate-preview-tests/src/test/java/com/github/takahirom/ExampleUnitTest.kt similarity index 81% rename from sample-generate-preview-tests/src/test/java/com/github/takahirom/sample/ExampleUnitTest.kt rename to sample-generate-preview-tests/src/test/java/com/github/takahirom/ExampleUnitTest.kt index 6289244c0..be2beb3c7 100644 --- a/sample-generate-preview-tests/src/test/java/com/github/takahirom/sample/ExampleUnitTest.kt +++ b/sample-generate-preview-tests/src/test/java/com/github/takahirom/ExampleUnitTest.kt @@ -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). * From c6e8413af8e0c02ddfa413883ca6e0a9b7d85086 Mon Sep 17 00:00:00 2001 From: takahirom Date: Fri, 13 Dec 2024 19:11:44 +0900 Subject: [PATCH 3/6] Use captureScreenRoboImage for dialog --- .../com/github/takahirom/roborazzi/RoborazziCompose.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 057bbf3ed..df91a1c0f 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 @@ -8,9 +8,7 @@ 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 androidx.test.espresso.Espresso -import org.robolectric.Shadows import java.io.File @@ -117,8 +115,10 @@ private fun ActivityScenario.captureRoboImage( val viewRootForTest = composeView.getChildAt(0) as ViewRootForTest viewRootForTest.view.captureRoboImage(file, roborazziOptions) } else { - // For dialogs - captureRootsInternal(windowRoots.drop(1), roborazziOptions, file) + // Dialog case + roborazziReportLog("It seems that there are multiple windows." + + "We merge all windows using captureScreenRoboImage().") + captureScreenRoboImage(file, roborazziOptions) } } } \ No newline at end of file From 0b22865e5ff90a01c320f3a1b6b46064f2f2d9cc Mon Sep 17 00:00:00 2001 From: takahirom Date: Sat, 14 Dec 2024 12:57:41 +0900 Subject: [PATCH 4/6] Add PreviewDialogSurface --- .../takahirom/preview/tests/Previews.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) 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 5bdef3e82..776e3176f 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,7 +2,9 @@ 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 @@ -124,6 +126,24 @@ fun PreviewDialog() { } } +@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, From 3bb4ce4f068b9c688217db341a98e21f32e47159 Mon Sep 17 00:00:00 2001 From: takahirom Date: Sat, 14 Dec 2024 13:14:44 +0900 Subject: [PATCH 5/6] Add captureScreenIfMultipleWindows --- .../takahirom/roborazzi/RoborazziCompose.kt | 26 ++++----- .../github/takahirom/roborazzi/Roborazzi.kt | 55 ++++++++++++++----- 2 files changed, 52 insertions(+), 29 deletions(-) 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 df91a1c0f..23f92e6e8 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 @@ -106,19 +106,17 @@ private fun ActivityScenario.captureRoboImage( // Views needs to be laid out before we can capture them Espresso.onIdle() - val windowRoots = fetchRobolectricWindowRoots() - if (windowRoots.size <= 1) { - val composeView = activity.window.decorView - .findViewById(android.R.id.content) - .getChildAt(0) as ComposeView - @SuppressLint("VisibleForTests") - val viewRootForTest = composeView.getChildAt(0) as ViewRootForTest - viewRootForTest.view.captureRoboImage(file, roborazziOptions) - } else { - // Dialog case - roborazziReportLog("It seems that there are multiple windows." + - "We merge all windows using captureScreenRoboImage().") - captureScreenRoboImage(file, roborazziOptions) - } + captureScreenIfMultipleWindows( + file = file, + roborazziOptions = roborazziOptions, + captureSingleComponent = { + val composeView = activity.window.decorView + .findViewById(android.R.id.content) + .getChildAt(0) as ComposeView + @SuppressLint("VisibleForTests") + val viewRootForTest = composeView.getChildAt(0) as ViewRootForTest + viewRootForTest.view.captureRoboImage(file, roborazziOptions) + } + ) } } \ No newline at end of file diff --git a/roborazzi/src/main/java/com/github/takahirom/roborazzi/Roborazzi.kt b/roborazzi/src/main/java/com/github/takahirom/roborazzi/Roborazzi.kt index e86b2b86e..3a4212a88 100644 --- a/roborazzi/src/main/java/com/github/takahirom/roborazzi/Roborazzi.kt +++ b/roborazzi/src/main/java/com/github/takahirom/roborazzi/Roborazzi.kt @@ -173,19 +173,38 @@ fun captureRootsInternal( } } +@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 { 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") + @Suppress("INACCESSIBLE_TYPE") val listActiveRoots = + rootsOracle.javaClass.getMethod("listActiveRoots") listActiveRoots.isAccessible = true @Suppress("UNCHECKED_CAST", "INACCESSIBLE_TYPE") val roots: List = (listActiveRoots.invoke(rootsOracle) as List ).sortedBy { it.windowLayoutParams.get()?.type } return roots - } catch(e: Throwable) { + } catch (e: Throwable) { e.printStackTrace() return emptyList() } @@ -300,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( From d766c55c65b6904592c4a52d73bed5d2db5bca6f Mon Sep 17 00:00:00 2001 From: takahirom Date: Sat, 14 Dec 2024 14:13:08 +0900 Subject: [PATCH 6/6] Remove Espresso.onIdle for now (it seems no effect now) --- .../java/com/github/takahirom/roborazzi/RoborazziCompose.kt | 4 ---- 1 file changed, 4 deletions(-) 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 23f92e6e8..aef31da5d 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 @@ -8,7 +8,6 @@ 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 @@ -103,9 +102,6 @@ private fun ActivityScenario.captureRoboImage( onActivity { activity -> activity.setContent(content = { content() }) - - // Views needs to be laid out before we can capture them - Espresso.onIdle() captureScreenIfMultipleWindows( file = file, roborazziOptions = roborazziOptions,