From 26e162544d281ba7b1f9fe2f2b10c24fc51c433c Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sat, 23 Nov 2024 11:34:56 +0000 Subject: [PATCH 01/11] Run accessibility checks after each screenshot This commit introduces the ability to run accessibility checks after each screenshot is taken by Roborazzi. This is achieved by adding a new accessibility check strategy, `AccessibilityCheckEachScreenshotStrategy`, which is used by the `RoborazziRule`. Additionally, the `captureAndroidView` and `captureComposeNode` functions have been updated to accept an `onEach` callback, which is invoked after each screenshot is taken. This callback can be used to perform any necessary actions, such as running accessibility checks. Finally the checks are changed to only run when roborazzi is enabled and will be skipped completely if roborazzi is not running. --- .../RoborazziATFAccessibilityChecker.kt | 30 +++- .../takahirom/roborazzi/RoborazziRule.kt | 54 +++++-- .../github/takahirom/roborazzi/Roborazzi.kt | 33 +++-- .../sample/ComposeA11yAfterScreenshotTest.kt | 137 ++++++++++++++++++ .../roborazzi/sample/ComposeA11yTest.kt | 23 ++- .../sample/ComposeA11yWithCustomCheckTest.kt | 102 +++++++------ 6 files changed, 303 insertions(+), 76 deletions(-) create mode 100644 sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yAfterScreenshotTest.kt diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt index 6211d896..d53fd867 100644 --- a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt @@ -10,6 +10,8 @@ import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction import androidx.test.espresso.ViewInteraction +import com.github.takahirom.roborazzi.RoborazziRule.AccessibilityCheckStrategy +import com.github.takahirom.roborazzi.RoborazziRule.AccessibilityCheckStrategy.CheckPoint import com.github.takahirom.roborazzi.RoborazziRule.CaptureRoot import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType @@ -228,14 +230,14 @@ data class RoborazziATFAccessibilityChecker( companion object } -@ExperimentalRoborazziApi -data class AccessibilityCheckAfterTestStrategy( - val accessibilityOptionsFactory: () -> RoborazziATFAccessibilityCheckOptions = { provideATFAccessibilityOptionsOrCreateDefault() }, -) : RoborazziRule.AccessibilityCheckStrategy { +abstract class BaseAccessibilityCheckStrategy: AccessibilityCheckStrategy { + abstract val accessibilityOptionsFactory: () -> RoborazziATFAccessibilityCheckOptions + + private val accessibilityOptions by lazy { accessibilityOptionsFactory() } + override fun runAccessibilityChecks( captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions ) { - val accessibilityOptions = accessibilityOptionsFactory() accessibilityOptions .checker .runAccessibilityChecks( @@ -254,3 +256,21 @@ data class AccessibilityCheckAfterTestStrategy( ) } } + +@ExperimentalRoborazziApi +data class AccessibilityCheckEachScreenshotStrategy( + override val accessibilityOptionsFactory: () -> RoborazziATFAccessibilityCheckOptions = { provideATFAccessibilityOptionsOrCreateDefault() }, +) : BaseAccessibilityCheckStrategy() { + override fun shouldRunAt(checkPoint: CheckPoint): Boolean { + return checkPoint == CheckPoint.AfterScreenshot || checkPoint == CheckPoint.AfterTest + } +} + +@ExperimentalRoborazziApi +data class AccessibilityCheckAfterTestStrategy( + override val accessibilityOptionsFactory: () -> RoborazziATFAccessibilityCheckOptions = { provideATFAccessibilityOptionsOrCreateDefault() }, +) : BaseAccessibilityCheckStrategy() { + override fun shouldRunAt(checkPoint: CheckPoint): Boolean { + return checkPoint == CheckPoint.AfterTest + } +} diff --git a/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/RoborazziRule.kt b/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/RoborazziRule.kt index bbc6d497..a99cef37 100644 --- a/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/RoborazziRule.kt +++ b/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/RoborazziRule.kt @@ -3,6 +3,7 @@ package com.github.takahirom.roborazzi import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.test.espresso.ViewInteraction +import com.github.takahirom.roborazzi.RoborazziRule.AccessibilityCheckStrategy.CheckPoint import org.junit.rules.TestWatcher import org.junit.runner.Description import org.junit.runners.model.Statement @@ -67,6 +68,13 @@ class RoborazziRule private constructor( captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions, ) + + enum class CheckPoint { + AfterTest, AfterScreenshot + } + + fun shouldRunAt(checkPoint: CheckPoint): Boolean + // Use `roborazzi-accessibility-check`'s [com.github.takahirom.roborazzi.AccessibilityCheckAfterTestStrategy] data object None : AccessibilityCheckStrategy { override fun runAccessibilityChecks( @@ -75,6 +83,8 @@ class RoborazziRule private constructor( ) { // Do nothing } + + override fun shouldRunAt(checkPoint: CheckPoint): Boolean = false } } @@ -180,23 +190,15 @@ class RoborazziRule private constructor( description: Description, captureRoot: CaptureRoot ) { + val captureType = options.captureType val evaluate: () -> Unit = { try { - val accessibilityChecks = options.accessibilityCheckStrategy - // TODO enable a11y before showing content - + println("evaluate $captureType") base.evaluate() - - accessibilityChecks.runAccessibilityChecks( - captureRoot = captureRoot, - roborazziOptions = options.roborazziOptions - ) - } catch (e: Exception) { throw e } } - val captureType = options.captureType if (!options.roborazziOptions.taskType.isEnabled()) { evaluate() return @@ -219,16 +221,34 @@ class RoborazziRule private constructor( } is CaptureType.AllImage, is CaptureType.Gif -> { + val accessibilityChecks = options.accessibilityCheckStrategy + val result = when (captureRoot) { is CaptureRoot.Compose -> captureRoot.semanticsNodeInteraction.captureComposeNode( composeRule = captureRoot.composeRule, roborazziOptions = roborazziOptions, - block = evaluate + block = evaluate, + onEach = { + if (accessibilityChecks.shouldRunAt(CheckPoint.AfterScreenshot)) { + accessibilityChecks.runAccessibilityChecks( + captureRoot = captureRoot, + roborazziOptions = options.roborazziOptions + ) + } + }, ) is CaptureRoot.View -> captureRoot.viewInteraction.captureAndroidView( roborazziOptions = roborazziOptions, - block = evaluate + block = evaluate, + onEach = { + if (accessibilityChecks.shouldRunAt(CheckPoint.AfterScreenshot)) { + accessibilityChecks.runAccessibilityChecks( + captureRoot = captureRoot, + roborazziOptions = options.roborazziOptions + ) + } + }, ) CaptureRoot.None -> { @@ -260,7 +280,17 @@ class RoborazziRule private constructor( is CaptureType.LastImage -> { val result = runCatching { + val accessibilityChecks = options.accessibilityCheckStrategy + evaluate() + + println("Checking") + if (accessibilityChecks.shouldRunAt(CheckPoint.AfterTest)) { + accessibilityChecks.runAccessibilityChecks( + captureRoot = captureRoot, + roborazziOptions = options.roborazziOptions + ) + } } if (!captureType.onlyFail || result.isFailure) { val outputFile = 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 e4272554..2ce5ddc5 100644 --- a/roborazzi/src/main/java/com/github/takahirom/roborazzi/Roborazzi.kt +++ b/roborazzi/src/main/java/com/github/takahirom/roborazzi/Roborazzi.kt @@ -222,7 +222,7 @@ fun ViewInteraction.captureRoboGif( ) { // currently, gif compare is not supported if (!roborazziOptions.taskType.isRecording()) return - captureAndroidView(roborazziOptions, block).apply { + captureAndroidView(roborazziOptions = roborazziOptions, onEach = {}, block = block).apply { saveGif(file) clear() result.getOrThrow() @@ -244,7 +244,7 @@ fun ViewInteraction.captureRoboLastImage( block: () -> Unit ) { if (!roborazziOptions.taskType.isEnabled()) return - captureAndroidView(roborazziOptions, block).apply { + captureAndroidView(roborazziOptions = roborazziOptions, onEach = {}, block = block).apply { saveLastImage(file) clear() result.getOrThrow() @@ -257,7 +257,7 @@ fun ViewInteraction.captureRoboAllImage( block: () -> Unit ) { if (!roborazziOptions.taskType.isEnabled()) return - captureAndroidView(roborazziOptions, block).apply { + captureAndroidView(roborazziOptions = roborazziOptions, onEach = {}, block = block).apply { saveAllImage(fileNameCreator) clear() result.getOrThrow() @@ -321,7 +321,12 @@ fun SemanticsNodeInteraction.captureRoboGif( ) { // currently, gif compare is not supported if (!roborazziOptions.taskType.isRecording()) return - captureComposeNode(composeRule, roborazziOptions, block).apply { + captureComposeNode( + composeRule = composeRule, + roborazziOptions = roborazziOptions, + onEach = {}, + block = block + ).apply { saveGif(file) clear() result.getOrThrow() @@ -340,6 +345,7 @@ class CaptureInternalResult( @InternalRoborazziApi fun ViewInteraction.captureAndroidView( roborazziOptions: RoborazziOptions, + onEach: () -> Unit = {}, block: () -> Unit ): CaptureInternalResult { var removeListener = {} @@ -351,7 +357,9 @@ fun ViewInteraction.captureAndroidView( handler.postAtFrontOfQueue { this@captureAndroidView.perform( ImageCaptureViewAction(roborazziOptions) { canvas -> - canvases.addIfChanged(canvas, roborazziOptions) + if (canvases.addIfChanged(canvas, roborazziOptions)) { + onEach() + } } ) } @@ -404,7 +412,9 @@ fun ViewInteraction.captureAndroidView( // If there is already a screen, we should take the screenshot first not to miss the frame. perform( ImageCaptureViewAction(roborazziOptions) { canvas -> - canvases.addIfChanged(canvas, roborazziOptions) + if (canvases.addIfChanged(canvas, roborazziOptions)) { + onEach() + } } ) perform(viewTreeListenerAction) @@ -445,18 +455,20 @@ fun ViewInteraction.captureAndroidView( private fun MutableList.addIfChanged( next: AwtRoboCanvas, roborazziOptions: RoborazziOptions -) { +): Boolean { val prev = this.lastOrNull() ?: run { this.add(next) - return + return true } val differ: ImageComparator.ComparisonResult = prev.differ(next, 1.0, roborazziOptions.compareOptions.imageComparator) if (!roborazziOptions.compareOptions.resultValidator(differ)) { this.add(next) + return true } else { // If the image is not changed, we should release the image. next.release() + return false } } @@ -481,6 +493,7 @@ private fun saveLastImage( fun SemanticsNodeInteraction.captureComposeNode( composeRule: ComposeTestRule, roborazziOptions: RoborazziOptions = provideRoborazziContext().options, + onEach: () -> Unit = {}, block: () -> Unit ): CaptureInternalResult { val canvases = mutableListOf() @@ -493,7 +506,9 @@ fun SemanticsNodeInteraction.captureComposeNode( ), roborazziOptions = roborazziOptions ) { - canvases.addIfChanged(it, roborazziOptions) + if (canvases.addIfChanged(it, roborazziOptions)) { + onEach() + } } } val handler = Handler(Looper.getMainLooper()) diff --git a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yAfterScreenshotTest.kt b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yAfterScreenshotTest.kt new file mode 100644 index 00000000..a821c732 --- /dev/null +++ b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yAfterScreenshotTest.kt @@ -0,0 +1,137 @@ +package com.github.takahirom.roborazzi.sample + +import androidx.activity.ComponentActivity +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performClick +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.github.takahirom.roborazzi.AccessibilityCheckEachScreenshotStrategy +import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers +import com.github.takahirom.roborazzi.RoborazziATFAccessibilityCheckOptions +import com.github.takahirom.roborazzi.RoborazziATFAccessibilityChecker +import com.github.takahirom.roborazzi.RoborazziRule +import com.github.takahirom.roborazzi.RoborazziRule.Options +import com.github.takahirom.roborazzi.RoborazziTaskType +import com.github.takahirom.roborazzi.roborazziSystemPropertyTaskType +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType.INFO +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType.NOT_RUN +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult +import com.google.android.apps.common.testing.accessibility.framework.Parameters +import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchy +import com.google.android.apps.common.testing.accessibility.framework.uielement.ViewHierarchyElement +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExpectedException +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode + +/** + * Test demonstrating a completely custom ATF Check. Expected to be a niche usecase, but critical when required. + */ +@RunWith(AndroidJUnit4::class) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@Config(qualifiers = RobolectricDeviceQualifiers.Pixel4, sdk = [35]) +class ComposeA11yAfterScreenshotTest { + @Suppress("DEPRECATION") + @get:Rule(order = Int.MIN_VALUE) + var thrown: ExpectedException = ExpectedException.none() + + @get:Rule + val composeTestRule = createAndroidComposeRule() + + val textCollectingCheck = TextCollectingCheck() + + val taskType: RoborazziTaskType = roborazziSystemPropertyTaskType() + + @get:Rule + val roborazziRule = RoborazziRule( + composeRule = composeTestRule, + captureRoot = composeTestRule.onRoot(), + options = Options( + captureType = RoborazziRule.CaptureType.AllImage(), + roborazziAccessibilityOptions = RoborazziATFAccessibilityCheckOptions( + checker = RoborazziATFAccessibilityChecker( + checks = setOf(textCollectingCheck), + ), + failureLevel = RoborazziATFAccessibilityChecker.CheckLevel.Warning + ), + accessibilityCheckStrategy = AccessibilityCheckEachScreenshotStrategy(), + ) + ) + + class TextCollectingCheck : CustomAccessibilityHierarchyCheck("Text Collecting Check") { + val foundText = mutableListOf() + + override fun runCheckOnHierarchy( + hierarchy: AccessibilityHierarchy, + element: ViewHierarchyElement?, + parameters: Parameters? + ): List { + return getElementsToEvaluate(element, hierarchy).map { childElement -> + val text = childElement.text?.toString() + + if (text == null) { + result(childElement, NOT_RUN, 1, null) + } else { + foundText.add(text) + result(childElement, INFO, 3, null) + } + } + } + } + + @Test + fun takesScreenshots() { + val count = mutableIntStateOf(0) + + composeTestRule.setContent { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column(modifier = Modifier + .size(100.dp) + .background(Color.White)) { + Text("Clicks: ${count.intValue}", color = Color.Black) + Spacer(modifier = Modifier.size(10.dp * count.intValue)) + Button(onClick = { + count.intValue++ + }, modifier = Modifier.testTag("increment")) { + Icon(Icons.Filled.Add, contentDescription = null) + } + } + } + } + + composeTestRule.onNodeWithTag("increment").performClick() + composeTestRule.waitUntil { count.intValue == 1 } + + composeTestRule.onNodeWithTag("increment").performClick() + composeTestRule.waitUntil { count.intValue == 2 } + + if (taskType.isEnabled()) { + // Last check happens after test + assertEquals(listOf("Clicks: 0", "Clicks: 1"), textCollectingCheck.foundText) + } else { + assertEquals(listOf(), textCollectingCheck.foundText) + } + } +} + diff --git a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yTest.kt b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yTest.kt index 58406d54..f0527f50 100644 --- a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yTest.kt +++ b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yTest.kt @@ -8,8 +8,8 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size -import androidx.compose.material.Button -import androidx.compose.material.Text +import androidx.compose.material3.Button +import androidx.compose.material3.Text import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -27,8 +27,11 @@ import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RoborazziATFAccessibilityCheckOptions import com.github.takahirom.roborazzi.RoborazziATFAccessibilityChecker import com.github.takahirom.roborazzi.RoborazziRule +import com.github.takahirom.roborazzi.RoborazziRule.CaptureType import com.github.takahirom.roborazzi.RoborazziRule.Options +import com.github.takahirom.roborazzi.RoborazziTaskType import com.github.takahirom.roborazzi.checkRoboAccessibility +import com.github.takahirom.roborazzi.roborazziSystemPropertyTaskType import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesElements import com.google.android.apps.common.testing.accessibility.framework.matcher.ElementMatchers.withTestTag @@ -50,11 +53,14 @@ class ComposeA11yTest { @get:Rule val composeTestRule = createAndroidComposeRule() + val taskType: RoborazziTaskType = roborazziSystemPropertyTaskType() + @get:Rule val roborazziRule = RoborazziRule( composeRule = composeTestRule, captureRoot = composeTestRule.onRoot(), options = Options( + captureType = CaptureType.LastImage(), roborazziAccessibilityOptions = RoborazziATFAccessibilityCheckOptions( RoborazziATFAccessibilityChecker( preset = AccessibilityCheckPreset.LATEST, @@ -68,7 +74,9 @@ class ComposeA11yTest { @Test fun clickableWithoutSemantics() { - thrown.expectMessage("SpeakableTextPresentCheck") + if (taskType.isEnabled()) { + thrown.expectMessage("SpeakableTextPresentCheck") + } composeTestRule.setContent { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { @@ -83,7 +91,10 @@ class ComposeA11yTest { @Test fun boxWithEmptyContentDescription() { - thrown.expectMessage("SpeakableTextPresentCheck") + println(taskType) + if (taskType.isEnabled()) { + thrown.expectMessage("SpeakableTextPresentCheck") + } composeTestRule.setContent { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { @@ -149,7 +160,9 @@ class ComposeA11yTest { @Test fun faintText() { - thrown.expectMessage("TextContrastCheck") + if (taskType.isEnabled()) { + thrown.expectMessage("TextContrastCheck") + } composeTestRule.setContent { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { diff --git a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yWithCustomCheckTest.kt b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yWithCustomCheckTest.kt index b35f3285..d0dc01c2 100644 --- a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yWithCustomCheckTest.kt +++ b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yWithCustomCheckTest.kt @@ -5,7 +5,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size -import androidx.compose.material.Text +import androidx.compose.material3.Text import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -19,7 +19,10 @@ import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RoborazziATFAccessibilityCheckOptions import com.github.takahirom.roborazzi.RoborazziATFAccessibilityChecker import com.github.takahirom.roborazzi.RoborazziRule +import com.github.takahirom.roborazzi.RoborazziRule.CaptureType import com.github.takahirom.roborazzi.RoborazziRule.Options +import com.github.takahirom.roborazzi.RoborazziTaskType +import com.github.takahirom.roborazzi.roborazziSystemPropertyTaskType import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType.ERROR import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType.INFO @@ -55,11 +58,14 @@ class ComposeA11yWithCustomCheckTest { @get:Rule val composeTestRule = createAndroidComposeRule() + val taskType: RoborazziTaskType = roborazziSystemPropertyTaskType() + @get:Rule val roborazziRule = RoborazziRule( composeRule = composeTestRule, captureRoot = composeTestRule.onRoot(), options = Options( + captureType = CaptureType.LastImage(), roborazziAccessibilityOptions = RoborazziATFAccessibilityCheckOptions( checker = RoborazziATFAccessibilityChecker( checks = setOf(NoRedTextCheck()), @@ -74,19 +80,7 @@ class ComposeA11yWithCustomCheckTest { /** * Custom Check that demonstrates accessing the screenshot and element data. */ - class NoRedTextCheck : AccessibilityHierarchyCheck() { - override fun getHelpTopic(): String? = null - - override fun getCategory(): Category = Category.IMPLEMENTATION - - override fun getTitleMessage(locale: Locale): String = "No Red Text" - - override fun getMessageForResultData(locale: Locale, p1: Int, metadata: ResultMetadata?): String = - "No Red Text $metadata" - - override fun getShortMessageForResultData(locale: Locale, p1: Int, metadata: ResultMetadata?): String = - "No Red Text $metadata" - + class NoRedTextCheck : CustomAccessibilityHierarchyCheck("No Red Text") { override fun runCheckOnHierarchy( hierarchy: AccessibilityHierarchy, element: ViewHierarchyElement?, @@ -96,7 +90,7 @@ class ComposeA11yWithCustomCheckTest { val textColors = primaryTextColors(childElement, parameters) if (textColors == null) { - result(childElement, NOT_RUN, 1, null) + this.result(childElement, NOT_RUN, 1, null) } else if (textColors.find { it.isMostlyRed() } != null) { result(childElement, ERROR, 3, textColors) } else { @@ -133,40 +127,13 @@ class ComposeA11yWithCustomCheckTest { private fun Color.isMostlyRed(): Boolean { return red > 0.8f && blue < 0.2f && green < 0.2f } - - private fun result( - childElement: ViewHierarchyElement?, - result: AccessibilityCheckResultType, - resultId: Int, - textColors: Iterable? - ) = CustomAccessibilityHierarchyCheckResult( - this::class.java, - result, - childElement, - resultId, - HashMapResultMetadata().apply { - if (textColors != null) { - putString("textColors", textColors.joinToString { "0x${it.toArgb().toUInt().toString(16)}" }) - } - } - ) - } - - // TODO fix after https://github.com/google/Accessibility-Test-Framework-for-Android/issues/64 - class CustomAccessibilityHierarchyCheckResult( - val checkClass: Class, - type: AccessibilityCheckResultType, - element: ViewHierarchyElement?, - resultId: Int, - metadata: ResultMetadata? - ) : AccessibilityHierarchyCheckResult(checkClass, type, element, resultId, metadata) { - override fun getMessage(locale: Locale?): CharSequence = - (checkClass.newInstance()).getMessageForResult(locale, this) } @Test fun redText() { - thrown.expectMessage("NoRedTextCheck") + if (taskType.isEnabled()) { + thrown.expectMessage("NoRedTextCheck") + } composeTestRule.setContent { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { @@ -189,3 +156,48 @@ class ComposeA11yWithCustomCheckTest { } } +// TODO fix after https://github.com/google/Accessibility-Test-Framework-for-Android/issues/64 +class CustomAccessibilityHierarchyCheckResult( + val checkClass: Class, + type: AccessibilityCheckResultType, + element: ViewHierarchyElement?, + resultId: Int, + metadata: ResultMetadata? +) : AccessibilityHierarchyCheckResult(checkClass, type, element, resultId, metadata) { + override fun getMessage(locale: Locale?): CharSequence = + (checkClass.newInstance()).getMessageForResult(locale, this) +} + +abstract class CustomAccessibilityHierarchyCheck( + val name: String +) : AccessibilityHierarchyCheck() { + override fun getHelpTopic(): String? = null + + override fun getCategory(): Category = Category.IMPLEMENTATION + + override fun getTitleMessage(locale: Locale): String = name + + override fun getMessageForResultData(locale: Locale, p1: Int, metadata: ResultMetadata?): String = + "$name $metadata" + + override fun getShortMessageForResultData(locale: Locale, p1: Int, metadata: ResultMetadata?): String = + "$name $metadata" + + protected fun result( + childElement: ViewHierarchyElement?, + result: AccessibilityCheckResultType, + resultId: Int, + textColors: Iterable? + ) = CustomAccessibilityHierarchyCheckResult( + this::class.java, + result, + childElement, + resultId, + HashMapResultMetadata().apply { + if (textColors != null) { + putString("textColors", textColors.joinToString { "0x${it.toArgb().toUInt().toString(16)}" }) + } + } + ) +} + From e45dded1065a4d686be8646373f2b3fe4d3aeb48 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sat, 23 Nov 2024 11:36:50 +0000 Subject: [PATCH 02/11] Cleanup --- .../main/java/com/github/takahirom/roborazzi/RoborazziRule.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/RoborazziRule.kt b/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/RoborazziRule.kt index a99cef37..572e9796 100644 --- a/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/RoborazziRule.kt +++ b/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/RoborazziRule.kt @@ -190,15 +190,14 @@ class RoborazziRule private constructor( description: Description, captureRoot: CaptureRoot ) { - val captureType = options.captureType val evaluate: () -> Unit = { try { - println("evaluate $captureType") base.evaluate() } catch (e: Exception) { throw e } } + val captureType = options.captureType if (!options.roborazziOptions.taskType.isEnabled()) { evaluate() return @@ -284,7 +283,6 @@ class RoborazziRule private constructor( evaluate() - println("Checking") if (accessibilityChecks.shouldRunAt(CheckPoint.AfterTest)) { accessibilityChecks.runAccessibilityChecks( captureRoot = captureRoot, From e5d8ab79f27c435c3daf52bf7b8ec3de9c715dc8 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sat, 23 Nov 2024 13:11:21 +0000 Subject: [PATCH 03/11] Fix test --- .../takahirom/roborazzi/sample/ViewA11yTest.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ViewA11yTest.kt b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ViewA11yTest.kt index 7a8bad22..6e4bbdcc 100644 --- a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ViewA11yTest.kt +++ b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ViewA11yTest.kt @@ -17,7 +17,9 @@ import com.github.takahirom.roborazzi.RoborazziATFAccessibilityCheckOptions import com.github.takahirom.roborazzi.RoborazziATFAccessibilityChecker import com.github.takahirom.roborazzi.RoborazziRule import com.github.takahirom.roborazzi.RoborazziRule.Options +import com.github.takahirom.roborazzi.RoborazziTaskType import com.github.takahirom.roborazzi.checkRoboAccessibility +import com.github.takahirom.roborazzi.roborazziSystemPropertyTaskType import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesElements import com.google.android.apps.common.testing.accessibility.framework.matcher.ElementMatchers.withTestTag @@ -39,6 +41,8 @@ class ViewA11yTest { @get:Rule val activityScenarioRule = ActivityScenarioRule(ComponentActivity::class.java) + val taskType: RoborazziTaskType = roborazziSystemPropertyTaskType() + @get:Rule val roborazziRule = RoborazziRule( captureRoot = Espresso.onView(ViewMatchers.isRoot()), @@ -56,7 +60,9 @@ class ViewA11yTest { @Test fun clickableWithoutSemantics() { - thrown.expectMessage("SpeakableTextPresentCheck") + if (taskType.isEnabled()) { + thrown.expectMessage("SpeakableTextPresentCheck") + } activityScenarioRule.scenario.onActivity { activity -> activity.setContentView( @@ -159,7 +165,9 @@ class ViewA11yTest { @Test fun faintText() { - thrown.expectMessage("TextContrastCheck") + if (taskType.isEnabled()) { + thrown.expectMessage("TextContrastCheck") + } activityScenarioRule.scenario.onActivity { activity -> activity.setContentView( From 3578557cd4f16216de9501417b57ec692119fd8d Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sat, 23 Nov 2024 14:48:05 +0000 Subject: [PATCH 04/11] Fix test --- .../com/github/takahirom/roborazzi/sample/ViewA11yTest.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ViewA11yTest.kt b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ViewA11yTest.kt index 6e4bbdcc..83948452 100644 --- a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ViewA11yTest.kt +++ b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ViewA11yTest.kt @@ -16,6 +16,7 @@ import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RoborazziATFAccessibilityCheckOptions import com.github.takahirom.roborazzi.RoborazziATFAccessibilityChecker import com.github.takahirom.roborazzi.RoborazziRule +import com.github.takahirom.roborazzi.RoborazziRule.CaptureType import com.github.takahirom.roborazzi.RoborazziRule.Options import com.github.takahirom.roborazzi.RoborazziTaskType import com.github.takahirom.roborazzi.checkRoboAccessibility @@ -45,8 +46,9 @@ class ViewA11yTest { @get:Rule val roborazziRule = RoborazziRule( - captureRoot = Espresso.onView(ViewMatchers.isRoot()), + captureRoot = onView(ViewMatchers.isRoot()), options = Options( + captureType = CaptureType.LastImage(), roborazziAccessibilityOptions = RoborazziATFAccessibilityCheckOptions( checker = RoborazziATFAccessibilityChecker( preset = AccessibilityCheckPreset.LATEST, From 6bba1d979117c70cfc46fe91196283f10d852cf1 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sun, 24 Nov 2024 10:29:40 +0000 Subject: [PATCH 05/11] fix merge --- .../roborazzi/sample/ComposeA11yAfterScreenshotTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yAfterScreenshotTest.kt b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yAfterScreenshotTest.kt index a821c732..a9b6f856 100644 --- a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yAfterScreenshotTest.kt +++ b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yAfterScreenshotTest.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.test.performClick import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.AccessibilityCheckEachScreenshotStrategy +import com.github.takahirom.roborazzi.ExperimentalRoborazziApi import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RoborazziATFAccessibilityCheckOptions import com.github.takahirom.roborazzi.RoborazziATFAccessibilityChecker @@ -48,6 +49,7 @@ import org.robolectric.annotation.GraphicsMode /** * Test demonstrating a completely custom ATF Check. Expected to be a niche usecase, but critical when required. */ +@OptIn(ExperimentalRoborazziApi::class) @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config(qualifiers = RobolectricDeviceQualifiers.Pixel4, sdk = [35]) From ab53441f534aca7236bd8590979530676b68edb9 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sun, 24 Nov 2024 10:35:35 +0000 Subject: [PATCH 06/11] simplify API --- .../RoborazziATFAccessibilityChecker.kt | 19 +++--- .../takahirom/roborazzi/RoborazziRule.kt | 59 +++++-------------- 2 files changed, 27 insertions(+), 51 deletions(-) diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt index c545d28b..2c0ea388 100644 --- a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt @@ -11,7 +11,6 @@ import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction import androidx.test.espresso.ViewInteraction import com.github.takahirom.roborazzi.RoborazziRule.AccessibilityCheckStrategy -import com.github.takahirom.roborazzi.RoborazziRule.AccessibilityCheckStrategy.CheckPoint import com.github.takahirom.roborazzi.RoborazziRule.CaptureRoot import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType @@ -228,6 +227,7 @@ data class RoborazziATFAccessibilityChecker( AccessibilityCheckResultType.WARNING -> roborazziErrorLog( "Warning: $check" ) + AccessibilityCheckResultType.SUPPRESSED -> roborazziReportLog( "Suppressed: $check" ) @@ -249,12 +249,12 @@ data class RoborazziATFAccessibilityChecker( companion object } -abstract class BaseAccessibilityCheckStrategy: AccessibilityCheckStrategy { +abstract class BaseAccessibilityCheckStrategy : AccessibilityCheckStrategy { abstract val accessibilityOptionsFactory: () -> RoborazziATFAccessibilityCheckOptions private val accessibilityOptions by lazy { accessibilityOptionsFactory() } - override fun runAccessibilityChecks( + fun runAccessibilityChecks( captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions ) { accessibilityOptions @@ -280,8 +280,12 @@ abstract class BaseAccessibilityCheckStrategy: AccessibilityCheckStrategy { data class AccessibilityCheckEachScreenshotStrategy( override val accessibilityOptionsFactory: () -> RoborazziATFAccessibilityCheckOptions = { provideATFAccessibilityOptionsOrCreateDefault() }, ) : BaseAccessibilityCheckStrategy() { - override fun shouldRunAt(checkPoint: CheckPoint): Boolean { - return checkPoint == CheckPoint.AfterScreenshot || checkPoint == CheckPoint.AfterTest + override fun afterScreenshot(captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions) { + runAccessibilityChecks(captureRoot, roborazziOptions) + } + + override fun afterTest(captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions) { + runAccessibilityChecks(captureRoot, roborazziOptions) } } @@ -289,7 +293,8 @@ data class AccessibilityCheckEachScreenshotStrategy( data class AccessibilityCheckAfterTestStrategy( override val accessibilityOptionsFactory: () -> RoborazziATFAccessibilityCheckOptions = { provideATFAccessibilityOptionsOrCreateDefault() }, ) : BaseAccessibilityCheckStrategy() { - override fun shouldRunAt(checkPoint: CheckPoint): Boolean { - return checkPoint == CheckPoint.AfterTest + + override fun afterTest(captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions) { + runAccessibilityChecks(captureRoot, roborazziOptions) } } diff --git a/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/RoborazziRule.kt b/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/RoborazziRule.kt index 9726afd7..b3fdac5d 100644 --- a/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/RoborazziRule.kt +++ b/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/RoborazziRule.kt @@ -3,7 +3,6 @@ package com.github.takahirom.roborazzi import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.test.espresso.ViewInteraction -import com.github.takahirom.roborazzi.RoborazziRule.AccessibilityCheckStrategy.CheckPoint import org.junit.rules.TestWatcher import org.junit.runner.Description import org.junit.runners.model.Statement @@ -83,29 +82,11 @@ class RoborazziRule private constructor( @ExperimentalRoborazziApi interface AccessibilityCheckStrategy { - @InternalRoborazziApi - fun runAccessibilityChecks( - captureRoot: CaptureRoot, - roborazziOptions: RoborazziOptions, - ) - - enum class CheckPoint { - AfterTest, AfterScreenshot - } - - fun shouldRunAt(checkPoint: CheckPoint): Boolean + fun afterScreenshot(captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions) {} + fun afterTest(captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions) {} // Use `roborazzi-accessibility-check`'s [com.github.takahirom.roborazzi.AccessibilityCheckAfterTestStrategy] - data object None : AccessibilityCheckStrategy { - override fun runAccessibilityChecks( - captureRoot: CaptureRoot, - roborazziOptions: RoborazziOptions - ) { - // Do nothing - } - - override fun shouldRunAt(checkPoint: CheckPoint): Boolean = false - } + data object None : AccessibilityCheckStrategy } sealed interface CaptureType { @@ -240,20 +221,16 @@ class RoborazziRule private constructor( } is CaptureType.AllImage, is CaptureType.Gif -> { - val accessibilityChecks = options.accessibilityCheckStrategy - val result = when (captureRoot) { is CaptureRoot.Compose -> captureRoot.semanticsNodeInteraction.captureComposeNode( composeRule = captureRoot.composeRule, roborazziOptions = roborazziOptions, block = evaluate, onEach = { - if (accessibilityChecks.shouldRunAt(CheckPoint.AfterScreenshot)) { - accessibilityChecks.runAccessibilityChecks( - captureRoot = captureRoot, - roborazziOptions = options.roborazziOptions - ) - } + options.accessibilityCheckStrategy.afterScreenshot( + captureRoot = captureRoot, + roborazziOptions = options.roborazziOptions + ) }, ) @@ -261,12 +238,10 @@ class RoborazziRule private constructor( roborazziOptions = roborazziOptions, block = evaluate, onEach = { - if (accessibilityChecks.shouldRunAt(CheckPoint.AfterScreenshot)) { - accessibilityChecks.runAccessibilityChecks( - captureRoot = captureRoot, - roborazziOptions = options.roborazziOptions - ) - } + options.accessibilityCheckStrategy.afterScreenshot( + captureRoot = captureRoot, + roborazziOptions = options.roborazziOptions + ) }, ) @@ -299,16 +274,12 @@ class RoborazziRule private constructor( is CaptureType.LastImage -> { val result = runCatching { - val accessibilityChecks = options.accessibilityCheckStrategy - evaluate() - if (accessibilityChecks.shouldRunAt(CheckPoint.AfterTest)) { - accessibilityChecks.runAccessibilityChecks( - captureRoot = captureRoot, - roborazziOptions = options.roborazziOptions - ) - } + options.accessibilityCheckStrategy.afterTest( + captureRoot = captureRoot, + roborazziOptions = options.roborazziOptions + ) } if (!captureType.onlyFail || result.isFailure) { val outputFile = From fbd34bf0f5db9b28a5a51fa2de1534985046c1d9 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sun, 24 Nov 2024 10:36:43 +0000 Subject: [PATCH 07/11] simplify API --- .../takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt index 2c0ea388..525f420d 100644 --- a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt @@ -79,7 +79,6 @@ data class RoborazziATFAccessibilityChecker( data class Compose(val semanticsNodeInteraction: SemanticsNodeInteraction) : CheckNode } - @ExperimentalRoborazziApi enum class CheckLevel(private vararg val failedTypes: AccessibilityCheckResultType) { Error(AccessibilityCheckResultType.ERROR), @@ -249,6 +248,7 @@ data class RoborazziATFAccessibilityChecker( companion object } +@ExperimentalRoborazziApi abstract class BaseAccessibilityCheckStrategy : AccessibilityCheckStrategy { abstract val accessibilityOptionsFactory: () -> RoborazziATFAccessibilityCheckOptions From 7913478f4035fddd62aca1246cb596bd119ae820 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sun, 24 Nov 2024 10:37:52 +0000 Subject: [PATCH 08/11] cleanup --- .../com/github/takahirom/roborazzi/sample/ComposeA11yTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yTest.kt b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yTest.kt index 422b9cd6..a6d2cadb 100644 --- a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yTest.kt +++ b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yTest.kt @@ -93,7 +93,6 @@ class ComposeA11yTest { @Test fun boxWithEmptyContentDescription() { - println(taskType) if (taskType.isEnabled()) { thrown.expectMessage("SpeakableTextPresentCheck") } From 254600b78edff819231a14cd326ce969b7ccd2a1 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sun, 24 Nov 2024 13:45:32 +0000 Subject: [PATCH 09/11] Update roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt Co-authored-by: Takahiro Menju --- .../takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt index 525f420d..38accca8 100644 --- a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt @@ -254,6 +254,7 @@ abstract class BaseAccessibilityCheckStrategy : AccessibilityCheckStrategy { private val accessibilityOptions by lazy { accessibilityOptionsFactory() } + @InternalRoborazziApi fun runAccessibilityChecks( captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions ) { From 86b4ca14cb58641ca3cde3fb9d0e8840631fb036 Mon Sep 17 00:00:00 2001 From: takahirom Date: Mon, 25 Nov 2024 10:07:21 +0900 Subject: [PATCH 10/11] Rename CaptureRoot to RuleCaptureRoot to clarify the naming --- .../RoborazziATFAccessibilityChecker.kt | 28 +++++------ .../takahirom/roborazzi/RoborazziRule.kt | 48 +++++++++---------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt index 38accca8..2a0c1ca4 100644 --- a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt @@ -11,7 +11,7 @@ import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction import androidx.test.espresso.ViewInteraction import com.github.takahirom.roborazzi.RoborazziRule.AccessibilityCheckStrategy -import com.github.takahirom.roborazzi.RoborazziRule.CaptureRoot +import com.github.takahirom.roborazzi.RoborazziRule.RuleCaptureRoot import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck @@ -256,19 +256,19 @@ abstract class BaseAccessibilityCheckStrategy : AccessibilityCheckStrategy { @InternalRoborazziApi fun runAccessibilityChecks( - captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions + ruleCaptureRoot: RuleCaptureRoot, roborazziOptions: RoborazziOptions ) { accessibilityOptions .checker .runAccessibilityChecks( - checkNode = when (captureRoot) { - is CaptureRoot.Compose -> RoborazziATFAccessibilityChecker.CheckNode.Compose( - semanticsNodeInteraction = captureRoot.semanticsNodeInteraction + checkNode = when (ruleCaptureRoot) { + is RuleCaptureRoot.Compose -> RoborazziATFAccessibilityChecker.CheckNode.Compose( + semanticsNodeInteraction = ruleCaptureRoot.semanticsNodeInteraction ) - CaptureRoot.None -> return - is CaptureRoot.View -> RoborazziATFAccessibilityChecker.CheckNode.View( - viewInteraction = captureRoot.viewInteraction + RuleCaptureRoot.None -> return + is RuleCaptureRoot.View -> RoborazziATFAccessibilityChecker.CheckNode.View( + viewInteraction = ruleCaptureRoot.viewInteraction ) }, roborazziOptions = roborazziOptions, @@ -281,12 +281,12 @@ abstract class BaseAccessibilityCheckStrategy : AccessibilityCheckStrategy { data class AccessibilityCheckEachScreenshotStrategy( override val accessibilityOptionsFactory: () -> RoborazziATFAccessibilityCheckOptions = { provideATFAccessibilityOptionsOrCreateDefault() }, ) : BaseAccessibilityCheckStrategy() { - override fun afterScreenshot(captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions) { - runAccessibilityChecks(captureRoot, roborazziOptions) + override fun afterScreenshot(ruleCaptureRoot: RuleCaptureRoot, roborazziOptions: RoborazziOptions) { + runAccessibilityChecks(ruleCaptureRoot, roborazziOptions) } - override fun afterTest(captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions) { - runAccessibilityChecks(captureRoot, roborazziOptions) + override fun afterTest(ruleCaptureRoot: RuleCaptureRoot, roborazziOptions: RoborazziOptions) { + runAccessibilityChecks(ruleCaptureRoot, roborazziOptions) } } @@ -295,7 +295,7 @@ data class AccessibilityCheckAfterTestStrategy( override val accessibilityOptionsFactory: () -> RoborazziATFAccessibilityCheckOptions = { provideATFAccessibilityOptionsOrCreateDefault() }, ) : BaseAccessibilityCheckStrategy() { - override fun afterTest(captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions) { - runAccessibilityChecks(captureRoot, roborazziOptions) + override fun afterTest(ruleCaptureRoot: RuleCaptureRoot, roborazziOptions: RoborazziOptions) { + runAccessibilityChecks(ruleCaptureRoot, roborazziOptions) } } diff --git a/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/RoborazziRule.kt b/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/RoborazziRule.kt index b3fdac5d..81b2b38a 100644 --- a/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/RoborazziRule.kt +++ b/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/RoborazziRule.kt @@ -25,7 +25,7 @@ private val defaultFileProvider: FileProvider = * 2. Capture screenshots for each test when specifying RoborazziRule.options.captureType. */ class RoborazziRule private constructor( - private val captureRoot: CaptureRoot, + private val ruleCaptureRoot: RuleCaptureRoot, private val options: Options = Options() ) : TestWatcher() { init { @@ -82,8 +82,8 @@ class RoborazziRule private constructor( @ExperimentalRoborazziApi interface AccessibilityCheckStrategy { - fun afterScreenshot(captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions) {} - fun afterTest(captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions) {} + fun afterScreenshot(ruleCaptureRoot: RuleCaptureRoot, roborazziOptions: RoborazziOptions) {} + fun afterTest(ruleCaptureRoot: RuleCaptureRoot, roborazziOptions: RoborazziOptions) {} // Use `roborazzi-accessibility-check`'s [com.github.takahirom.roborazzi.AccessibilityCheckAfterTestStrategy] data object None : AccessibilityCheckStrategy @@ -127,21 +127,21 @@ class RoborazziRule private constructor( } @InternalRoborazziApi - sealed interface CaptureRoot { - object None : CaptureRoot + sealed interface RuleCaptureRoot { + object None : RuleCaptureRoot class Compose( val composeRule: ComposeTestRule, val semanticsNodeInteraction: SemanticsNodeInteraction - ) : CaptureRoot + ) : RuleCaptureRoot - class View(val viewInteraction: ViewInteraction) : CaptureRoot + class View(val viewInteraction: ViewInteraction) : RuleCaptureRoot } constructor( captureRoot: ViewInteraction, options: Options = Options() ) : this( - captureRoot = CaptureRoot.View(captureRoot), + ruleCaptureRoot = RuleCaptureRoot.View(captureRoot), options = options ) @@ -150,14 +150,14 @@ class RoborazziRule private constructor( captureRoot: SemanticsNodeInteraction, options: Options = Options() ) : this( - captureRoot = CaptureRoot.Compose(composeRule, captureRoot), + ruleCaptureRoot = RuleCaptureRoot.Compose(composeRule, captureRoot), options = options ) constructor( options: Options = Options() ) : this( - captureRoot = CaptureRoot.None, + ruleCaptureRoot = RuleCaptureRoot.None, options = options ) @@ -174,7 +174,7 @@ class RoborazziRule private constructor( provideRoborazziContext().setRuleOverrideFileProvider(options.outputFileProvider) provideRoborazziContext().setRuleOverrideDescription(description) provideRoborazziContext().setRuleOverrideAccessibilityOptions(options.roborazziAccessibilityOptions) - runTest(base, description, captureRoot) + runTest(base, description, ruleCaptureRoot) } finally { provideRoborazziContext().clearRuleOverrideOutputDirectory() provideRoborazziContext().clearRuleOverrideRoborazziOptions() @@ -189,7 +189,7 @@ class RoborazziRule private constructor( private fun runTest( base: Statement, description: Description, - captureRoot: CaptureRoot + ruleCaptureRoot: RuleCaptureRoot ) { val evaluate: () -> Unit = { try { @@ -221,31 +221,31 @@ class RoborazziRule private constructor( } is CaptureType.AllImage, is CaptureType.Gif -> { - val result = when (captureRoot) { - is CaptureRoot.Compose -> captureRoot.semanticsNodeInteraction.captureComposeNode( - composeRule = captureRoot.composeRule, + val result = when (ruleCaptureRoot) { + is RuleCaptureRoot.Compose -> ruleCaptureRoot.semanticsNodeInteraction.captureComposeNode( + composeRule = ruleCaptureRoot.composeRule, roborazziOptions = roborazziOptions, block = evaluate, onEach = { options.accessibilityCheckStrategy.afterScreenshot( - captureRoot = captureRoot, + ruleCaptureRoot = ruleCaptureRoot, roborazziOptions = options.roborazziOptions ) }, ) - is CaptureRoot.View -> captureRoot.viewInteraction.captureAndroidView( + is RuleCaptureRoot.View -> ruleCaptureRoot.viewInteraction.captureAndroidView( roborazziOptions = roborazziOptions, block = evaluate, onEach = { options.accessibilityCheckStrategy.afterScreenshot( - captureRoot = captureRoot, + ruleCaptureRoot = ruleCaptureRoot, roborazziOptions = options.roborazziOptions ) }, ) - CaptureRoot.None -> { + RuleCaptureRoot.None -> { error("captureRoot is required for AllImage and Gif") } } @@ -277,25 +277,25 @@ class RoborazziRule private constructor( evaluate() options.accessibilityCheckStrategy.afterTest( - captureRoot = captureRoot, + ruleCaptureRoot = ruleCaptureRoot, roborazziOptions = options.roborazziOptions ) } if (!captureType.onlyFail || result.isFailure) { val outputFile = fileWithRecordFilePathStrategy(DefaultFileNameGenerator.generateFilePath()) - when (captureRoot) { - is CaptureRoot.Compose -> captureRoot.semanticsNodeInteraction.captureRoboImage( + when (ruleCaptureRoot) { + is RuleCaptureRoot.Compose -> ruleCaptureRoot.semanticsNodeInteraction.captureRoboImage( file = outputFile, roborazziOptions = roborazziOptions ) - is CaptureRoot.View -> captureRoot.viewInteraction.captureRoboImage( + is RuleCaptureRoot.View -> ruleCaptureRoot.viewInteraction.captureRoboImage( file = outputFile, roborazziOptions = roborazziOptions ) - CaptureRoot.None -> { + RuleCaptureRoot.None -> { error("captureRoot is required for LastImage") } } From a48585b802175522c43f54c4fe7308d00bfa8ce4 Mon Sep 17 00:00:00 2001 From: takahirom Date: Mon, 25 Nov 2024 10:34:31 +0900 Subject: [PATCH 11/11] Add RoboComponent parameter to onCanvas --- .../github/takahirom/roborazzi/Roborazzi.kt | 22 ++++++++++--------- .../github/takahirom/roborazzi/captureDump.kt | 4 ++-- 2 files changed, 14 insertions(+), 12 deletions(-) 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 2ce5ddc5..e481157b 100644 --- a/roborazzi/src/main/java/com/github/takahirom/roborazzi/Roborazzi.kt +++ b/roborazzi/src/main/java/com/github/takahirom/roborazzi/Roborazzi.kt @@ -53,7 +53,7 @@ fun ViewInteraction.captureRoboImage( roborazziOptions: RoborazziOptions = provideRoborazziContext().options, ) { if (!roborazziOptions.taskType.isEnabled()) return - perform(ImageCaptureViewAction(roborazziOptions) { canvas -> + perform(ImageCaptureViewAction(roborazziOptions) { _, canvas -> processOutputImageAndReportWithDefaults( canvas = canvas, goldenFile = file, @@ -149,7 +149,8 @@ fun captureScreenRoboImage( // 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 + @Suppress("UNCHECKED_CAST") val roots: List = + listActiveRoots.invoke(rootsOracle) as List debugLog { "captureScreenRoboImage roots: ${roots.joinToString("\n") { it.toString() }}" } @@ -159,7 +160,7 @@ fun captureScreenRoboImage( roborazziOptions = roborazziOptions ), roborazziOptions = roborazziOptions, - ) { canvas -> + ) { _, canvas -> processOutputImageAndReportWithDefaults( canvas = canvas, goldenFile = file, @@ -284,7 +285,7 @@ fun SemanticsNodeInteraction.captureRoboImage( roborazziOptions = roborazziOptions ), roborazziOptions = roborazziOptions, - ) { canvas -> + ) { _, canvas -> processOutputImageAndReportWithDefaults( canvas = canvas, goldenFile = file, @@ -356,7 +357,7 @@ fun ViewInteraction.captureAndroidView( val listener = ViewTreeObserver.OnGlobalLayoutListener { handler.postAtFrontOfQueue { this@captureAndroidView.perform( - ImageCaptureViewAction(roborazziOptions) { canvas -> + ImageCaptureViewAction(roborazziOptions) { _, canvas -> if (canvases.addIfChanged(canvas, roborazziOptions)) { onEach() } @@ -411,7 +412,7 @@ fun ViewInteraction.captureAndroidView( try { // If there is already a screen, we should take the screenshot first not to miss the frame. perform( - ImageCaptureViewAction(roborazziOptions) { canvas -> + ImageCaptureViewAction(roborazziOptions) { _, canvas -> if (canvases.addIfChanged(canvas, roborazziOptions)) { onEach() } @@ -505,8 +506,8 @@ fun SemanticsNodeInteraction.captureComposeNode( roborazziOptions ), roborazziOptions = roborazziOptions - ) { - if (canvases.addIfChanged(it, roborazziOptions)) { + ) { _, canvas -> + if (canvases.addIfChanged(canvas, roborazziOptions)) { onEach() } } @@ -592,7 +593,7 @@ private fun saveAllImage( private class ImageCaptureViewAction( val roborazziOptions: RoborazziOptions, - val saveAction: (AwtRoboCanvas) -> Unit + val saveAction: (RoboComponent, AwtRoboCanvas) -> Unit ) : ViewAction { override fun getConstraints(): Matcher { @@ -618,7 +619,7 @@ private class ImageCaptureViewAction( internal fun capture( rootComponent: RoboComponent, roborazziOptions: RoborazziOptions, - onCanvas: (AwtRoboCanvas) -> Unit + onCanvas: (RoboComponent, AwtRoboCanvas) -> Unit ) { when (roborazziOptions.captureType) { is Dump -> captureDump( @@ -632,6 +633,7 @@ internal fun capture( val image = rootComponent.image ?: throw IllegalStateException("Unable to find the image of the target root component. Does the rendering element exist?") onCanvas( + rootComponent, AwtRoboCanvas( width = image.width, height = image.height, diff --git a/roborazzi/src/main/java/com/github/takahirom/roborazzi/captureDump.kt b/roborazzi/src/main/java/com/github/takahirom/roborazzi/captureDump.kt index 24fcb38c..bd330e72 100644 --- a/roborazzi/src/main/java/com/github/takahirom/roborazzi/captureDump.kt +++ b/roborazzi/src/main/java/com/github/takahirom/roborazzi/captureDump.kt @@ -23,7 +23,7 @@ internal fun captureDump( rootComponent: RoboComponent, dumpOptions: Dump, recordOptions: RoborazziOptions.RecordOptions, - onCanvas: (AwtRoboCanvas) -> Unit + onCanvas: (RoboComponent, AwtRoboCanvas) -> Unit ) { // val start = System.currentTimeMillis() val basicSize = dumpOptions.basicSize @@ -155,7 +155,7 @@ internal fun captureDump( } } bfs() - onCanvas(canvas) + onCanvas(rootComponent, canvas) // val end = System.currentTimeMillis() // println("roborazzi takes " + (end - start) + "ms") }