From 79be1a5f5d988454a701630cec35e7944d4f35d7 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sat, 16 Nov 2024 13:16:53 +0000 Subject: [PATCH 01/34] Add basic Compose Android A11y checks Run ATF after test completes to check for A11y issues. --- gradle/libs.versions.toml | 4 +- .../takahirom/roborazzi/RoborazziOptions.kt | 14 +++ roborazzi-junit-rule/build.gradle | 2 + .../com/github/takahirom/roborazzi/ATF.kt | 16 +++ .../takahirom/roborazzi/RoborazziRule.kt | 61 +++++++++- .../roborazzi/sample/ComposeA11yTest.kt | 112 ++++++++++++++++++ 6 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/ATF.kt create mode 100644 sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1d030df6e..48b30092d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,4 +1,5 @@ [versions] +accessibilityTestFramework = "4.1.1" javaToolchain = "17" javaTarget = "11" agp = "8.6.1" @@ -7,7 +8,7 @@ kotlin = "1.9.22" mavenPublish = "0.25.3" composeCompiler = "1.5.10" composeMultiplatform = "1.6.2" -robolectric = "4.12.2" +robolectric = "4.14" generativeaiGoogle = "0.9.0-1.0.1" robolectric-android-all = "Q-robolectric-5415296" @@ -49,6 +50,7 @@ webpImageio = "0.3.3" composable-preview-scanner = "0.4.0" [libraries] +accessibility-test-framework = { module = "com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework", version.ref = "accessibilityTestFramework" } roborazzi = { module = "io.github.takahirom.roborazzi:roborazzi", version.ref = "roborazzi-for-replacing-by-include-build" } roborazzi-junit-rule = { module = "io.github.takahirom.roborazzi:roborazzi-junit-rule", version.ref = "roborazzi-for-replacing-by-include-build" } roborazzi-rule = { module = "io.github.takahirom.roborazzi:roborazzi-rule", version.ref = "roborazzi-for-replacing-by-include-build" } diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt index 230ec4400..9fdd44c80 100644 --- a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt +++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt @@ -236,8 +236,22 @@ data class RoborazziOptions( val applyDeviceCrop: Boolean = false, val pixelBitConfig: PixelBitConfig = PixelBitConfig.Argb8888, val imageIoFormat: ImageIoFormat = ImageIoFormat(), + val accessibilityChecks: AccessibilityChecks = AccessibilityChecks.Disabled ) + interface AccessibilityChecker { + companion object + } + + @ExperimentalRoborazziApi + sealed interface AccessibilityChecks { + data class Validate( + val checker: AccessibilityChecker + ) : AccessibilityChecks + + data object Disabled : AccessibilityChecks + } + enum class PixelBitConfig { Argb8888, Rgb565; diff --git a/roborazzi-junit-rule/build.gradle b/roborazzi-junit-rule/build.gradle index 8c9a8b642..199055d78 100644 --- a/roborazzi-junit-rule/build.gradle +++ b/roborazzi-junit-rule/build.gradle @@ -39,7 +39,9 @@ android { dependencies { implementation project(':roborazzi') implementation libs.androidx.test.ext.junit.ktx + compileOnly libs.robolectric compileOnly libs.androidx.test.ext.junit.ktx compileOnly libs.androidx.compose.ui.test compileOnly libs.androidx.compose.ui.test.junit4 + api libs.accessibility.test.framework } \ No newline at end of file diff --git a/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/ATF.kt b/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/ATF.kt new file mode 100644 index 000000000..29dd91497 --- /dev/null +++ b/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/ATF.kt @@ -0,0 +1,16 @@ +package com.github.takahirom.roborazzi + +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset +import com.google.android.apps.common.testing.accessibility.framework.integrations.espresso.AccessibilityValidator + +data class ATFAccessibilityChecker( + val accessibilityValidator: AccessibilityValidator +) : RoborazziOptions.AccessibilityChecker + +fun RoborazziOptions.AccessibilityChecker.Companion.atf( + preset: AccessibilityCheckPreset? = AccessibilityCheckPreset.LATEST, + config: AccessibilityValidator.() -> Unit +) = + ATFAccessibilityChecker(accessibilityValidator = AccessibilityValidator().apply { + setCheckPreset(preset) + }.apply(config)) \ No newline at end of file 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 521819723..e0524812b 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 @@ -1,11 +1,17 @@ package com.github.takahirom.roborazzi +import android.annotation.SuppressLint +import androidx.compose.ui.platform.ViewRootForTest import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.test.espresso.ViewInteraction +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult +import com.google.android.apps.common.testing.accessibility.framework.integrations.espresso.AccessibilityValidator import org.junit.rules.TestWatcher import org.junit.runner.Description import org.junit.runners.model.Statement +import org.robolectric.shadows.ShadowBuild import java.io.File private val defaultFileProvider: FileProvider = @@ -157,9 +163,14 @@ class RoborazziRule private constructor( description: Description, captureRoot: CaptureRoot ) { - val evaluate = { + val evaluate: () -> Unit = { try { + val accessibilityValidator: AccessibilityValidator? = findActiveAccessibilityValidator() + // TODO enable a11y before showing content + base.evaluate() + + accessibilityValidator?.runAccessibilityChecks(captureRoot) } catch (e: Exception) { throw e } @@ -256,4 +267,52 @@ class RoborazziRule private constructor( } } + + @SuppressLint("VisibleForTests") + private fun AccessibilityValidator.runAccessibilityChecks( + captureRoot: CaptureRoot, + ) { + // TODO remove this once ATF doesn't bail out + // https://github.com/google/Accessibility-Test-Framework-for-Android/blob/c65cab02b2a845c29c3da100d6adefd345a144e3/src/main/java/com/google/android/apps/common/testing/accessibility/framework/uielement/AccessibilityHierarchyAndroid.java#L667 + ShadowBuild.setFingerprint("roborazzi") + + if (captureRoot is CaptureRoot.Compose) { + setRunChecksFromRootView(true) + val view = (captureRoot.semanticsNodeInteraction.fetchSemanticsNode().root as ViewRootForTest).view.rootView + + // Will throw based on configuration + val results = checkAndReturnResults(view) + + // Report on any warnings in the log output if not failing + results.forEach { check -> + when (check.accessibilityHierarchyCheckResult.type) { + AccessibilityCheckResultType.ERROR -> System.err.println("Error: " + check.explain()) + AccessibilityCheckResultType.WARNING -> System.err.println( + "Warning: " + check.explain() + ) + + AccessibilityCheckResultType.INFO -> println( + "Info: " + check.explain() + ) + + else -> {} + } + } + // TODO handle View cases +// } else if (captureRoot is CaptureRoot.View) { + } + } + + private fun AccessibilityViewCheckResult.explain() = "" + accessibilityHierarchyCheckResult.getShortMessage( + java.util.Locale.getDefault() + ) + " " + this.element + + private fun findActiveAccessibilityValidator(): AccessibilityValidator? { + var accessibilityValidator: AccessibilityValidator? = null + val accessibilityChecks = options.roborazziOptions.recordOptions.accessibilityChecks + if (accessibilityChecks is RoborazziOptions.AccessibilityChecks.Validate) { + accessibilityValidator = (accessibilityChecks.checker as? ATFAccessibilityChecker)?.accessibilityValidator + } + return accessibilityValidator + } } \ No newline at end of file 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 new file mode 100644 index 000000000..eaa43b8e5 --- /dev/null +++ b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yTest.kt @@ -0,0 +1,112 @@ +package com.github.takahirom.roborazzi.sample + +import androidx.activity.ComponentActivity +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers +import com.github.takahirom.roborazzi.RoborazziOptions +import com.github.takahirom.roborazzi.RoborazziOptions.AccessibilityChecker +import com.github.takahirom.roborazzi.RoborazziOptions.AccessibilityChecks +import com.github.takahirom.roborazzi.RoborazziOptions.RecordOptions +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.atf +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType +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 + +@RunWith(AndroidJUnit4::class) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@Config(qualifiers = RobolectricDeviceQualifiers.Pixel4, sdk = [35]) +class ComposeA11yTest { + @Suppress("DEPRECATION") + @get:Rule(order = Int.MIN_VALUE) + var thrown: ExpectedException = ExpectedException.none() + + @get:Rule + val composeTestRule = createAndroidComposeRule() + + @get:Rule + val roborazziRule = RoborazziRule( + composeRule = composeTestRule, captureRoot = composeTestRule.onRoot(), options = Options( + captureType = CaptureType.LastImage(), roborazziOptions = RoborazziOptions( + recordOptions = RecordOptions( + applyDeviceCrop = true, accessibilityChecks = AccessibilityChecks.Validate( + checker = AccessibilityChecker.atf(preset = AccessibilityCheckPreset.LATEST) { + setThrowExceptionFor(AccessibilityCheckResultType.WARNING) + }) + ), + ) + ) + ) + + @Test + fun clickableWithoutSemantics() { + thrown.expectMessage("SpeakableTextPresentCheck") + + composeTestRule.setContent { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Box(Modifier.size(48.dp).background(Color.Black).clickable {}) + } + } + } + + @Test + fun boxWithEmptyContentDescription() { + thrown.expectMessage("SpeakableTextPresentCheck") + + composeTestRule.setContent { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Box(Modifier.size(48.dp).background(Color.Black).semantics { + contentDescription = "" + }) + } + } + } + + @Test + fun smallClickable() { + // TODO check why not failing +// thrown.expectMessage("sdffsd") + + composeTestRule.setContent { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Button(onClick = {}, modifier = Modifier.size(30.dp)) { + Text("Something to Click") + } + } + } + } + + @Test + fun clickableBox() { + composeTestRule.setContent { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Button(onClick = {}) { + Text("Something to Click") + } + } + } + } +} + From 3ca5cce44d2e5f465fb7c6d9f07dab6dbafbba9c Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sat, 16 Nov 2024 13:23:15 +0000 Subject: [PATCH 02/34] Use ignore for problem test --- .../com/github/takahirom/roborazzi/sample/ComposeA11yTest.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 eaa43b8e5..695cf299c 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 @@ -28,6 +28,7 @@ import com.github.takahirom.roborazzi.RoborazziRule.Options import com.github.takahirom.roborazzi.atf import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.ExpectedException @@ -85,9 +86,9 @@ class ComposeA11yTest { } @Test + @Ignore("TODO investigate why not failing") fun smallClickable() { - // TODO check why not failing -// thrown.expectMessage("sdffsd") + thrown.expect(Exception::class.java) composeTestRule.setContent { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { From 7ff09aa40662d09f1e51d864ab4753e2413750ea Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sat, 16 Nov 2024 15:24:03 +0000 Subject: [PATCH 03/34] Improve the implementation --- .../takahirom/roborazzi/RoborazziOptions.kt | 14 --- .../com/github/takahirom/roborazzi/ATF.kt | 116 ++++++++++++++++-- .../takahirom/roborazzi/RoborazziRule.kt | 88 +++++-------- .../roborazzi/sample/ComposeA11yTest.kt | 73 +++++++++-- 4 files changed, 202 insertions(+), 89 deletions(-) diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt index 9fdd44c80..230ec4400 100644 --- a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt +++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt @@ -236,22 +236,8 @@ data class RoborazziOptions( val applyDeviceCrop: Boolean = false, val pixelBitConfig: PixelBitConfig = PixelBitConfig.Argb8888, val imageIoFormat: ImageIoFormat = ImageIoFormat(), - val accessibilityChecks: AccessibilityChecks = AccessibilityChecks.Disabled ) - interface AccessibilityChecker { - companion object - } - - @ExperimentalRoborazziApi - sealed interface AccessibilityChecks { - data class Validate( - val checker: AccessibilityChecker - ) : AccessibilityChecks - - data object Disabled : AccessibilityChecks - } - enum class PixelBitConfig { Argb8888, Rgb565; diff --git a/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/ATF.kt b/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/ATF.kt index 29dd91497..ab59fb1db 100644 --- a/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/ATF.kt +++ b/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/ATF.kt @@ -1,16 +1,114 @@ package com.github.takahirom.roborazzi +import android.annotation.SuppressLint +import android.view.View +import androidx.annotation.RequiresApi +import androidx.compose.ui.platform.ViewRootForTest +import com.github.takahirom.roborazzi.RoborazziRule.ATFAccessibilityChecker +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.integrations.espresso.AccessibilityValidator +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult +import com.google.android.apps.common.testing.accessibility.framework.Parameters +import com.google.android.apps.common.testing.accessibility.framework.ViewChecker +import com.google.android.apps.common.testing.accessibility.framework.integrations.espresso.AccessibilityViewCheckException +import com.google.android.apps.common.testing.accessibility.framework.utils.contrast.BitmapImage +import org.hamcrest.Matcher +import org.hamcrest.Matchers +import org.robolectric.shadows.ShadowBuild -data class ATFAccessibilityChecker( - val accessibilityValidator: AccessibilityValidator -) : RoborazziOptions.AccessibilityChecker -fun RoborazziOptions.AccessibilityChecker.Companion.atf( +@SuppressLint("VisibleForTests") +@RequiresApi(34) +internal fun ATFAccessibilityChecker.runAccessibilityChecks( + captureRoot: CaptureRoot, + roborazziOptions: RoborazziOptions, +) { + // TODO remove this once ATF doesn't bail out + // https://github.com/google/Accessibility-Test-Framework-for-Android/blob/c65cab02b2a845c29c3da100d6adefd345a144e3/src/main/java/com/google/android/apps/common/testing/accessibility/framework/uielement/AccessibilityHierarchyAndroid.java#L667 + ShadowBuild.setFingerprint("roborazzi") + + if (captureRoot is CaptureRoot.Compose) { + val view = (captureRoot.semanticsNodeInteraction.fetchSemanticsNode().root as ViewRootForTest).view.rootView + + // Will throw based on configuration + val results = runAllChecks(roborazziOptions, view, captureRoot) + + // Report on any warnings in the log output if not failing + results.forEach { check -> + when (check.type) { + AccessibilityCheckResultType.ERROR -> System.err.println("Error: $check") + AccessibilityCheckResultType.WARNING -> System.err.println( + "Warning: $check" + ) + + AccessibilityCheckResultType.INFO -> println( + "Info: $check" + ) + + else -> {} + } + } + + val failures = results.filter { it.type.ordinal <= failureLevel.ordinal } + if (failures.isNotEmpty()) { + throw AccessibilityViewCheckException(failures.toMutableList()) + } + + // TODO handle View cases +// } else if (captureRoot is CaptureRoot.View) { + } +} + +@RequiresApi(34) +internal fun ATFAccessibilityChecker.runAllChecks( + roborazziOptions: RoborazziOptions, + view: View, + captureRoot: CaptureRoot.Compose +): List { + val screenshot = + RoboComponent.Compose(captureRoot.semanticsNodeInteraction.fetchSemanticsNode(), roborazziOptions).image + + val parameters = Parameters().apply { + if (screenshot != null) { + putScreenCapture(BitmapImage(screenshot)) + } + setSaveViewImages(true) + } + + val viewChecker = ViewChecker().apply { + setObtainCharacterLocations(true) + } + + val preset = AccessibilityCheckPreset.LATEST + val checks = AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset(preset) + + val results = viewChecker.runChecksOnView(checks, view, parameters) + + return results.filter { + !suppressions.matches(it) + } +} + +fun ATFAccessibilityChecker.Companion.atf( preset: AccessibilityCheckPreset? = AccessibilityCheckPreset.LATEST, - config: AccessibilityValidator.() -> Unit + suppressions: Matcher = Matchers.not(Matchers.anything()), + failureLevel: AccessibilityCheckResultType = AccessibilityCheckResultType.ERROR, +) = + ATFAccessibilityChecker( + checks = AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset(preset), + suppressions = suppressions, + failureLevel = failureLevel + ) + +fun ATFAccessibilityChecker.Companion.atf( + checks: Set, + suppressions: Matcher = Matchers.not(Matchers.anything()), + failureLevel: AccessibilityCheckResultType = AccessibilityCheckResultType.ERROR, ) = - ATFAccessibilityChecker(accessibilityValidator = AccessibilityValidator().apply { - setCheckPreset(preset) - }.apply(config)) \ No newline at end of file + ATFAccessibilityChecker( + checks = checks, + suppressions = suppressions, + failureLevel = failureLevel + ) 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 e0524812b..0bbb7008f 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 @@ -1,17 +1,16 @@ package com.github.takahirom.roborazzi -import android.annotation.SuppressLint -import androidx.compose.ui.platform.ViewRootForTest +import android.os.Build import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.test.espresso.ViewInteraction import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult -import com.google.android.apps.common.testing.accessibility.framework.integrations.espresso.AccessibilityValidator +import org.hamcrest.Matcher import org.junit.rules.TestWatcher import org.junit.runner.Description import org.junit.runners.model.Statement -import org.robolectric.shadows.ShadowBuild import java.io.File private val defaultFileProvider: FileProvider = @@ -62,8 +61,27 @@ class RoborazziRule private constructor( val outputFileProvider: FileProvider = provideRoborazziContext().fileProvider ?: defaultFileProvider, val roborazziOptions: RoborazziOptions = provideRoborazziContext().options, + val accessibilityChecks: AccessibilityChecks = AccessibilityChecks.Disabled, ) + @ExperimentalRoborazziApi + data class ATFAccessibilityChecker( + val checks: Set, + val suppressions: Matcher, + val failureLevel: AccessibilityCheckResultType, + ) { + companion object + } + + @ExperimentalRoborazziApi + sealed interface AccessibilityChecks { + data class Validate( + val checker: ATFAccessibilityChecker + ) : AccessibilityChecks + + data object Disabled : AccessibilityChecks + } + sealed interface CaptureType { /** * Do not generate images. Just provide the image path to [captureRoboImage]. @@ -165,12 +183,21 @@ class RoborazziRule private constructor( ) { val evaluate: () -> Unit = { try { - val accessibilityValidator: AccessibilityValidator? = findActiveAccessibilityValidator() + val accessibilityValidator = ((options.accessibilityChecks as? AccessibilityChecks.Validate)?.checker) // TODO enable a11y before showing content base.evaluate() - accessibilityValidator?.runAccessibilityChecks(captureRoot) + if (accessibilityValidator != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + accessibilityValidator.runAccessibilityChecks( + captureRoot = captureRoot, + roborazziOptions = options.roborazziOptions + ) + } else { + System.err.println("Skipping accessibilityChecks on API " + Build.VERSION.SDK_INT + "(< ${Build.VERSION_CODES.UPSIDE_DOWN_CAKE})") + } + } } catch (e: Exception) { throw e } @@ -265,54 +292,5 @@ class RoborazziRule private constructor( } } } - - } - - @SuppressLint("VisibleForTests") - private fun AccessibilityValidator.runAccessibilityChecks( - captureRoot: CaptureRoot, - ) { - // TODO remove this once ATF doesn't bail out - // https://github.com/google/Accessibility-Test-Framework-for-Android/blob/c65cab02b2a845c29c3da100d6adefd345a144e3/src/main/java/com/google/android/apps/common/testing/accessibility/framework/uielement/AccessibilityHierarchyAndroid.java#L667 - ShadowBuild.setFingerprint("roborazzi") - - if (captureRoot is CaptureRoot.Compose) { - setRunChecksFromRootView(true) - val view = (captureRoot.semanticsNodeInteraction.fetchSemanticsNode().root as ViewRootForTest).view.rootView - - // Will throw based on configuration - val results = checkAndReturnResults(view) - - // Report on any warnings in the log output if not failing - results.forEach { check -> - when (check.accessibilityHierarchyCheckResult.type) { - AccessibilityCheckResultType.ERROR -> System.err.println("Error: " + check.explain()) - AccessibilityCheckResultType.WARNING -> System.err.println( - "Warning: " + check.explain() - ) - - AccessibilityCheckResultType.INFO -> println( - "Info: " + check.explain() - ) - - else -> {} - } - } - // TODO handle View cases -// } else if (captureRoot is CaptureRoot.View) { - } - } - - private fun AccessibilityViewCheckResult.explain() = "" + accessibilityHierarchyCheckResult.getShortMessage( - java.util.Locale.getDefault() - ) + " " + this.element - - private fun findActiveAccessibilityValidator(): AccessibilityValidator? { - var accessibilityValidator: AccessibilityValidator? = null - val accessibilityChecks = options.roborazziOptions.recordOptions.accessibilityChecks - if (accessibilityChecks is RoborazziOptions.AccessibilityChecks.Validate) { - accessibilityValidator = (accessibilityChecks.checker as? ATFAccessibilityChecker)?.accessibilityValidator - } - return accessibilityValidator } } \ No newline at end of file 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 695cf299c..4a7d89708 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 @@ -11,24 +11,28 @@ import androidx.compose.material.Text 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.semantics.contentDescription import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RoborazziOptions -import com.github.takahirom.roborazzi.RoborazziOptions.AccessibilityChecker -import com.github.takahirom.roborazzi.RoborazziOptions.AccessibilityChecks import com.github.takahirom.roborazzi.RoborazziOptions.RecordOptions import com.github.takahirom.roborazzi.RoborazziRule +import com.github.takahirom.roborazzi.RoborazziRule.ATFAccessibilityChecker +import com.github.takahirom.roborazzi.RoborazziRule.AccessibilityChecks import com.github.takahirom.roborazzi.RoborazziRule.CaptureType import com.github.takahirom.roborazzi.RoborazziRule.Options import com.github.takahirom.roborazzi.atf import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType -import org.junit.Ignore +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesElements +import com.google.android.apps.common.testing.accessibility.framework.matcher.ElementMatchers.withTestTag import org.junit.Rule import org.junit.Test import org.junit.rules.ExpectedException @@ -47,16 +51,24 @@ class ComposeA11yTest { @get:Rule val composeTestRule = createAndroidComposeRule() + val atfAccessibilityChecker = ATFAccessibilityChecker.atf( + preset = AccessibilityCheckPreset.LATEST, + failureLevel = AccessibilityCheckResultType.WARNING, + suppressions = matchesElements(withTestTag("suppress")) + ) + @get:Rule val roborazziRule = RoborazziRule( - composeRule = composeTestRule, captureRoot = composeTestRule.onRoot(), options = Options( + composeRule = composeTestRule, + captureRoot = composeTestRule.onRoot(), + options = Options( captureType = CaptureType.LastImage(), roborazziOptions = RoborazziOptions( recordOptions = RecordOptions( - applyDeviceCrop = true, accessibilityChecks = AccessibilityChecks.Validate( - checker = AccessibilityChecker.atf(preset = AccessibilityCheckPreset.LATEST) { - setThrowExceptionFor(AccessibilityCheckResultType.WARNING) - }) + applyDeviceCrop = true ), + ), + accessibilityChecks = AccessibilityChecks.Validate( + checker = atfAccessibilityChecker ) ) ) @@ -86,17 +98,21 @@ class ComposeA11yTest { } @Test - @Ignore("TODO investigate why not failing") fun smallClickable() { - thrown.expect(Exception::class.java) + // for(ViewHierarchyElement view : getElementsToEvaluate(fromRoot, hierarchy)) { + // if (!Boolean.TRUE.equals(view.isClickable()) && !Boolean.TRUE.equals(view.isLongClickable())) { + // TODO investigate +// thrown.expectMessage("TouchTargetSizeCheck") composeTestRule.setContent { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Button(onClick = {}, modifier = Modifier.size(30.dp)) { + Button(onClick = {}, modifier = Modifier.size(30.dp).testTag("clickable")) { Text("Something to Click") } } } + + composeTestRule.onNodeWithTag("clickable").assertHasClickAction() } @Test @@ -109,5 +125,40 @@ class ComposeA11yTest { } } } + + @Test + fun supressionsTakeEffect() { + composeTestRule.setContent { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Box(Modifier.size(48.dp).background(Color.Black).testTag("suppress").semantics { + contentDescription = "" + }) + } + } + } + + @Test + fun faintText() { + thrown.expectMessage("TextContrastCheck") + + composeTestRule.setContent { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Box(modifier = Modifier.size(100.dp).background(Color.DarkGray)) { + Text("Something hard to read", color = Color.DarkGray) + } + } + } + } + + @Test + fun normalText() { + composeTestRule.setContent { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Box(modifier = Modifier.size(100.dp).background(Color.DarkGray)) { + Text("Something hard to read", color = Color.White) + } + } + } + } } From addf887f40a0903c2809566d9f468bcc24ef928a Mon Sep 17 00:00:00 2001 From: takahirom Date: Sun, 17 Nov 2024 10:47:26 +0900 Subject: [PATCH 04/34] Refactor log output to use centralized functions --- .../takahirom/roborazzi/ViewScreenshot.kt | 6 ++--- .../roborazzi/processOutputImageAndReport.kt | 6 ++--- .../roborazzi/reportLog.commonJvm.kt | 5 ++++ .../github/takahirom/roborazzi/reportLog.kt | 6 ++++- .../takahirom/roborazzi/reportLog.ios.kt | 10 ++++++++ .../takahirom/roborazzi/RoborazziIos.kt | 23 +++++++++++-------- .../com/github/takahirom/roborazzi/ATF.kt | 6 ++--- .../takahirom/roborazzi/RoborazziRule.kt | 2 +- .../github/takahirom/roborazzi/Roborazzi.kt | 2 +- 9 files changed, 44 insertions(+), 22 deletions(-) create mode 100644 include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/reportLog.commonJvm.kt create mode 100644 include-build/roborazzi-core/src/iosMain/kotlin/com/github/takahirom/roborazzi/reportLog.ios.kt diff --git a/include-build/roborazzi-core/src/androidMain/kotlin/com/github/takahirom/roborazzi/ViewScreenshot.kt b/include-build/roborazzi-core/src/androidMain/kotlin/com/github/takahirom/roborazzi/ViewScreenshot.kt index 6fee14b30..971548a63 100644 --- a/include-build/roborazzi-core/src/androidMain/kotlin/com/github/takahirom/roborazzi/ViewScreenshot.kt +++ b/include-build/roborazzi-core/src/androidMain/kotlin/com/github/takahirom/roborazzi/ViewScreenshot.kt @@ -82,7 +82,7 @@ private fun View.generateBitmap( val destBitmap = Bitmap.createBitmap(width, height, pixelBitConfig.toBitmapConfig()) when { Build.VERSION.SDK_INT < 26 -> { - println( + roborazziErrorLog( "**Warning from Roborazzi**: Robolectric may not function properly under API 26, " + "specifically it may fail to capture accurate screenshots. " + "Please add @Config(sdk = [26]) or higher to your test class to ensure proper operation. " + @@ -115,7 +115,7 @@ private fun View.generateBitmap( val window = nullableWindow!! if (Build.VERSION.SDK_INT < 28) { // See: https://github.com/robolectric/robolectric/blob/robolectric-4.10.3/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPixelCopy.java#L32 - println( + roborazziReportLog( "PixelCopy is not supported for API levels below 28. Falling back to View#draw instead of PixelCopy. " + "Consider using API level 28 or higher, e.g., @Config(sdk = [28])." ) @@ -124,7 +124,7 @@ private fun View.generateBitmap( generateBitmapFromPixelCopy(window, destBitmap, bitmapFuture) } } else { - println( + roborazziReportLog( "View.captureToImage Could not find window for view. Falling back to View#draw instead of PixelCopy" + "(If you are using Material3 dialogs, this is expected).", ) diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/processOutputImageAndReport.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/processOutputImageAndReport.kt index d0b3cd613..6a6962b4b 100644 --- a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/processOutputImageAndReport.kt +++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/processOutputImageAndReport.kt @@ -44,7 +44,7 @@ fun processOutputImageAndReport( "\ngoldenFile:${goldenFile.absolutePath}" } if (taskType.isEnabled() && !roborazziSystemPropertyTaskType().isEnabled()) { - println( + roborazziReportLog( "Roborazzi Warning:\n" + "You have specified '$taskType' without the necessary plugin configuration like roborazzi.test.record=true or ./gradlew recordRoborazziDebug.\n" + "This may complicate your screenshot testing process because the behavior is not changeable. And it doesn't allow Roborazzi plugin to generate test report.\n" + @@ -98,10 +98,10 @@ fun processOutputImageAndReport( ) diffPercentage = comparisonResult.pixelDifferences.toFloat() / comparisonResult.pixelCount val changed = !compareOptions.resultValidator(comparisonResult) - reportLog("${goldenFile.name} The differ result :$comparisonResult changed:$changed") + roborazziReportLog("${goldenFile.name} The differ result :$comparisonResult changed:$changed") changed } else { - reportLog("${goldenFile.name} The image size is changed. actual = (${goldenRoboCanvas.width}, ${goldenRoboCanvas.height}), golden = (${newRoboCanvas.croppedWidth}, ${newRoboCanvas.croppedHeight})") + roborazziReportLog("${goldenFile.name} The image size is changed. actual = (${goldenRoboCanvas.width}, ${goldenRoboCanvas.height}), golden = (${newRoboCanvas.croppedWidth}, ${newRoboCanvas.croppedHeight})") true } diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/reportLog.commonJvm.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/reportLog.commonJvm.kt new file mode 100644 index 000000000..1a135c175 --- /dev/null +++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/reportLog.commonJvm.kt @@ -0,0 +1,5 @@ +package com.github.takahirom.roborazzi + +actual fun roborazziErrorLog(message: String) { + System.err.println("Roborazzi: $message") +} \ No newline at end of file diff --git a/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/reportLog.kt b/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/reportLog.kt index 72f9e9cff..dc5ba0d69 100644 --- a/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/reportLog.kt +++ b/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/reportLog.kt @@ -1,5 +1,9 @@ package com.github.takahirom.roborazzi -fun reportLog(message: String) { +@InternalRoborazziApi +fun roborazziReportLog(message: String) { println("Roborazzi: $message") } + +@InternalRoborazziApi +expect fun roborazziErrorLog(message: String) \ No newline at end of file diff --git a/include-build/roborazzi-core/src/iosMain/kotlin/com/github/takahirom/roborazzi/reportLog.ios.kt b/include-build/roborazzi-core/src/iosMain/kotlin/com/github/takahirom/roborazzi/reportLog.ios.kt new file mode 100644 index 000000000..6dd40c5bb --- /dev/null +++ b/include-build/roborazzi-core/src/iosMain/kotlin/com/github/takahirom/roborazzi/reportLog.ios.kt @@ -0,0 +1,10 @@ +package com.github.takahirom.roborazzi + +import kotlinx.cinterop.ExperimentalForeignApi +import platform.posix.fprintf +import platform.posix.stderr + +@OptIn(ExperimentalForeignApi::class) +actual fun roborazziErrorLog(message: String) { + fprintf(stderr, "Roborazzi: %s\n", message) +} \ No newline at end of file diff --git a/roborazzi-compose-ios/src/iosMain/kotlin/io/github/takahirom/roborazzi/RoborazziIos.kt b/roborazzi-compose-ios/src/iosMain/kotlin/io/github/takahirom/roborazzi/RoborazziIos.kt index f23c3a0e1..b4b6c4c28 100644 --- a/roborazzi-compose-ios/src/iosMain/kotlin/io/github/takahirom/roborazzi/RoborazziIos.kt +++ b/roborazzi-compose-ios/src/iosMain/kotlin/io/github/takahirom/roborazzi/RoborazziIos.kt @@ -1,3 +1,5 @@ +@file:OptIn(InternalRoborazziApi::class) + package io.github.takahirom.roborazzi import androidx.compose.ui.graphics.PixelMap @@ -9,9 +11,10 @@ import androidx.compose.ui.test.captureToImage import com.github.takahirom.roborazzi.CaptureResult import com.github.takahirom.roborazzi.CaptureResults import com.github.takahirom.roborazzi.ExperimentalRoborazziApi +import com.github.takahirom.roborazzi.InternalRoborazziApi import com.github.takahirom.roborazzi.RoborazziTaskType import com.github.takahirom.roborazzi.getReportFileName -import com.github.takahirom.roborazzi.reportLog +import com.github.takahirom.roborazzi.roborazziReportLog import com.github.takahirom.roborazzi.roborazziSystemPropertyOutputDirectory import com.github.takahirom.roborazzi.roborazziSystemPropertyProjectPath import com.github.takahirom.roborazzi.roborazziSystemPropertyResultDirectory @@ -209,7 +212,7 @@ private fun unpremultiplyAlpha(cgImage: CGImageRef): CGImageRef? { newImage: UIImage ): CGImageRef? { if (goldenImage == null) { - reportLog("CompareImage Golden image is null") + roborazziReportLog("CompareImage Golden image is null") return newImage.CIImage?.CGImage } @@ -240,7 +243,7 @@ private fun unpremultiplyAlpha(cgImage: CGImageRef): CGImageRef? { bitmapInfo ) if (context == null) { - reportLog("CompareImage Failed to create context") + roborazziReportLog("CompareImage Failed to create context") return null } @@ -333,7 +336,7 @@ private fun unpremultiplyAlpha(cgImage: CGImageRef): CGImageRef? { val cgBitmapContextCreateImage = CGBitmapContextCreateImage(context) if (cgBitmapContextCreateImage == null) { - reportLog("CompareImage Failed to create image") + roborazziReportLog("CompareImage Failed to create image") } return cgBitmapContextCreateImage } @@ -379,7 +382,7 @@ private fun unpremultiplyAlpha(cgImage: CGImageRef): CGImageRef? { newPixelIndex = newPixelIndex, ) ) { - reportLog("Pixel changed at ($x, $y) from rgba(${goldenPtr[goldenPixelIndex]}, ${goldenPtr[goldenPixelIndex + 1]}, ${goldenPtr[goldenPixelIndex + 2]}, ${goldenPtr[goldenPixelIndex + 3]}) to rgba(${newPtr[newPixelIndex]}, ${newPtr[newPixelIndex + 1]}, ${newPtr[newPixelIndex + 2]}, ${newPtr[newPixelIndex + 3]})") + roborazziReportLog("Pixel changed at ($x, $y) from rgba(${goldenPtr[goldenPixelIndex]}, ${goldenPtr[goldenPixelIndex + 1]}, ${goldenPtr[goldenPixelIndex + 2]}, ${goldenPtr[goldenPixelIndex + 3]}) to rgba(${newPtr[newPixelIndex]}, ${newPtr[newPixelIndex + 1]}, ${newPtr[newPixelIndex + 2]}, ${newPtr[newPixelIndex + 3]})") val stringBuilder = StringBuilder() // properties @@ -419,7 +422,7 @@ private fun unpremultiplyAlpha(cgImage: CGImageRef): CGImageRef? { ) ) stringBuilder.appendLine("new CGImageGetBytesPerRow" + CGImageGetBytesPerRow(newCgImage)) - reportLog(stringBuilder.toString()) + roborazziReportLog(stringBuilder.toString()) return true } @@ -697,7 +700,7 @@ private fun writeImage(newImage: UIImage, path: String) { path, true ) - reportLog("Image is saved $path") + roborazziReportLog("Image is saved $path") } private fun loadGoldenImage( @@ -709,11 +712,11 @@ private fun loadGoldenImage( @Suppress("USELESS_CAST") val image: UIImage? = UIImage(filePath) as UIImage? if (image == null) { - reportLog("can't load reference image from $filePath") + roborazziReportLog("can't load reference image from $filePath") } val goldenImage = image ?.let { convertImageFormat(it) } if (goldenImage == null) { - reportLog("can't convert reference image from $filePath") + roborazziReportLog("can't convert reference image from $filePath") } return goldenImage } @@ -759,7 +762,7 @@ private fun writeJson( ), atomically = true ) - reportLog( + roborazziReportLog( "Report file is saved ${ getReportFileName( absolutePath = resultsDir, diff --git a/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/ATF.kt b/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/ATF.kt index ab59fb1db..a07ea84b0 100644 --- a/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/ATF.kt +++ b/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/ATF.kt @@ -38,12 +38,12 @@ internal fun ATFAccessibilityChecker.runAccessibilityChecks( // Report on any warnings in the log output if not failing results.forEach { check -> when (check.type) { - AccessibilityCheckResultType.ERROR -> System.err.println("Error: $check") - AccessibilityCheckResultType.WARNING -> System.err.println( + AccessibilityCheckResultType.ERROR -> roborazziErrorLog("Error: $check") + AccessibilityCheckResultType.WARNING -> roborazziErrorLog( "Warning: $check" ) - AccessibilityCheckResultType.INFO -> println( + AccessibilityCheckResultType.INFO -> roborazziReportLog( "Info: $check" ) 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 0bbb7008f..fdae76987 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 @@ -195,7 +195,7 @@ class RoborazziRule private constructor( roborazziOptions = options.roborazziOptions ) } else { - System.err.println("Skipping accessibilityChecks on API " + Build.VERSION.SDK_INT + "(< ${Build.VERSION_CODES.UPSIDE_DOWN_CAKE})") + roborazziErrorLog("Skipping accessibilityChecks on API " + Build.VERSION.SDK_INT + "(< ${Build.VERSION_CODES.UPSIDE_DOWN_CAKE})") } } } catch (e: Exception) { 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..e42725547 100644 --- a/roborazzi/src/main/java/com/github/takahirom/roborazzi/Roborazzi.kt +++ b/roborazzi/src/main/java/com/github/takahirom/roborazzi/Roborazzi.kt @@ -467,7 +467,7 @@ private fun saveLastImage( ) { val roboCanvas = canvases.lastOrNull() if (roboCanvas == null) { - println("Roborazzi could not capture for this test") + roborazziErrorLog("Roborazzi could not capture for this test") return } processOutputImageAndReportWithDefaults( From 2be749b03a0cb3336060d0852f9decac6cf32065 Mon Sep 17 00:00:00 2001 From: takahirom Date: Sun, 17 Nov 2024 11:39:35 +0900 Subject: [PATCH 05/34] Separate roborazzi-accessibility-check module --- roborazzi-accessibility-check/.gitignore | 1 + roborazzi-accessibility-check/build.gradle | 44 ++++++++++ .../consumer-rules.pro | 0 .../proguard-rules.pro | 21 +++++ .../roborazzi/ExampleInstrumentedTest.kt | 22 +++++ .../src/main/AndroidManifest.xml | 4 + .../com/github/takahirom/roborazzi/ATF.kt | 47 ----------- .../roborazzi/ATFAccessibilityChecker.kt | 81 +++++++++++++++++++ .../takahirom/roborazzi/ExampleUnitTest.kt | 16 ++++ roborazzi-junit-rule/build.gradle | 1 - .../takahirom/roborazzi/RoborazziRule.kt | 54 +++++-------- sample-android/build.gradle | 1 + .../roborazzi/sample/ComposeA11yTest.kt | 6 +- settings.gradle | 1 + 14 files changed, 216 insertions(+), 83 deletions(-) create mode 100644 roborazzi-accessibility-check/.gitignore create mode 100644 roborazzi-accessibility-check/build.gradle create mode 100644 roborazzi-accessibility-check/consumer-rules.pro create mode 100644 roborazzi-accessibility-check/proguard-rules.pro create mode 100644 roborazzi-accessibility-check/src/androidTest/java/com/github/takahirom/roborazzi/ExampleInstrumentedTest.kt create mode 100644 roborazzi-accessibility-check/src/main/AndroidManifest.xml rename {roborazzi-junit-rule => roborazzi-accessibility-check}/src/main/java/com/github/takahirom/roborazzi/ATF.kt (58%) create mode 100644 roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt create mode 100644 roborazzi-accessibility-check/src/test/java/com/github/takahirom/roborazzi/ExampleUnitTest.kt diff --git a/roborazzi-accessibility-check/.gitignore b/roborazzi-accessibility-check/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/roborazzi-accessibility-check/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/roborazzi-accessibility-check/build.gradle b/roborazzi-accessibility-check/build.gradle new file mode 100644 index 000000000..1c737358b --- /dev/null +++ b/roborazzi-accessibility-check/build.gradle @@ -0,0 +1,44 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} +if (System.getenv("INTEGRATION_TEST") != "true") { + pluginManager.apply("com.vanniktech.maven.publish") +} + + +android { + namespace 'com.github.takahirom.roborazzi.accessibility.check' + compileSdk 34 + + defaultConfig { + minSdk 21 + targetSdk 32 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + buildFeatures { + } + testOptions { + unitTests { + includeAndroidResources = true + } + } +} + +dependencies { + implementation project(':roborazzi-junit-rule') + implementation project(':roborazzi') + implementation libs.androidx.test.ext.junit.ktx + compileOnly libs.robolectric + compileOnly libs.androidx.compose.ui.test + compileOnly libs.androidx.compose.ui.test.junit4 + api libs.accessibility.test.framework +} \ No newline at end of file diff --git a/roborazzi-accessibility-check/consumer-rules.pro b/roborazzi-accessibility-check/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/roborazzi-accessibility-check/proguard-rules.pro b/roborazzi-accessibility-check/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/roborazzi-accessibility-check/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/roborazzi-accessibility-check/src/androidTest/java/com/github/takahirom/roborazzi/ExampleInstrumentedTest.kt b/roborazzi-accessibility-check/src/androidTest/java/com/github/takahirom/roborazzi/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..f06fdc650 --- /dev/null +++ b/roborazzi-accessibility-check/src/androidTest/java/com/github/takahirom/roborazzi/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.github.takahirom.roborazzi + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.github.takahirom.roborazzi.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/roborazzi-accessibility-check/src/main/AndroidManifest.xml b/roborazzi-accessibility-check/src/main/AndroidManifest.xml new file mode 100644 index 000000000..44008a433 --- /dev/null +++ b/roborazzi-accessibility-check/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/ATF.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt similarity index 58% rename from roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/ATF.kt rename to roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt index a07ea84b0..95b9c448d 100644 --- a/roborazzi-junit-rule/src/main/java/com/github/takahirom/roborazzi/ATF.kt +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt @@ -1,10 +1,7 @@ package com.github.takahirom.roborazzi -import android.annotation.SuppressLint import android.view.View import androidx.annotation.RequiresApi -import androidx.compose.ui.platform.ViewRootForTest -import com.github.takahirom.roborazzi.RoborazziRule.ATFAccessibilityChecker 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 @@ -12,55 +9,11 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult import com.google.android.apps.common.testing.accessibility.framework.Parameters import com.google.android.apps.common.testing.accessibility.framework.ViewChecker -import com.google.android.apps.common.testing.accessibility.framework.integrations.espresso.AccessibilityViewCheckException import com.google.android.apps.common.testing.accessibility.framework.utils.contrast.BitmapImage import org.hamcrest.Matcher import org.hamcrest.Matchers -import org.robolectric.shadows.ShadowBuild -@SuppressLint("VisibleForTests") -@RequiresApi(34) -internal fun ATFAccessibilityChecker.runAccessibilityChecks( - captureRoot: CaptureRoot, - roborazziOptions: RoborazziOptions, -) { - // TODO remove this once ATF doesn't bail out - // https://github.com/google/Accessibility-Test-Framework-for-Android/blob/c65cab02b2a845c29c3da100d6adefd345a144e3/src/main/java/com/google/android/apps/common/testing/accessibility/framework/uielement/AccessibilityHierarchyAndroid.java#L667 - ShadowBuild.setFingerprint("roborazzi") - - if (captureRoot is CaptureRoot.Compose) { - val view = (captureRoot.semanticsNodeInteraction.fetchSemanticsNode().root as ViewRootForTest).view.rootView - - // Will throw based on configuration - val results = runAllChecks(roborazziOptions, view, captureRoot) - - // Report on any warnings in the log output if not failing - results.forEach { check -> - when (check.type) { - AccessibilityCheckResultType.ERROR -> roborazziErrorLog("Error: $check") - AccessibilityCheckResultType.WARNING -> roborazziErrorLog( - "Warning: $check" - ) - - AccessibilityCheckResultType.INFO -> roborazziReportLog( - "Info: $check" - ) - - else -> {} - } - } - - val failures = results.filter { it.type.ordinal <= failureLevel.ordinal } - if (failures.isNotEmpty()) { - throw AccessibilityViewCheckException(failures.toMutableList()) - } - - // TODO handle View cases -// } else if (captureRoot is CaptureRoot.View) { - } -} - @RequiresApi(34) internal fun ATFAccessibilityChecker.runAllChecks( roborazziOptions: RoborazziOptions, diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt new file mode 100644 index 000000000..39031d821 --- /dev/null +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt @@ -0,0 +1,81 @@ +package com.github.takahirom.roborazzi + +import android.annotation.SuppressLint +import android.os.Build +import androidx.compose.ui.platform.ViewRootForTest +import com.github.takahirom.roborazzi.RoborazziRule.CaptureRoot +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult +import com.google.android.apps.common.testing.accessibility.framework.integrations.espresso.AccessibilityViewCheckException +import org.hamcrest.Matcher +import org.robolectric.shadows.ShadowBuild + +@ExperimentalRoborazziApi +data class ATFAccessibilityChecker( + val checks: Set, + val suppressions: Matcher, + val failureLevel: AccessibilityCheckResultType, +) { + @SuppressLint("VisibleForTests") + fun runAccessibilityChecks( + captureRoot: CaptureRoot, + roborazziOptions: RoborazziOptions, + ) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + roborazziErrorLog("Skipping accessibilityChecks on API " + Build.VERSION.SDK_INT + "(< ${Build.VERSION_CODES.UPSIDE_DOWN_CAKE})") + return + } + // TODO remove this once ATF doesn't bail out + // https://github.com/google/Accessibility-Test-Framework-for-Android/blob/c65cab02b2a845c29c3da100d6adefd345a144e3/src/main/java/com/google/android/apps/common/testing/accessibility/framework/uielement/AccessibilityHierarchyAndroid.java#L667 + ShadowBuild.setFingerprint("roborazzi") + + if (captureRoot is CaptureRoot.Compose) { + val view = + (captureRoot.semanticsNodeInteraction.fetchSemanticsNode().root as ViewRootForTest).view.rootView + + // Will throw based on configuration + val results = runAllChecks(roborazziOptions, view, captureRoot) + + // Report on any warnings in the log output if not failing + results.forEach { check -> + when (check.type) { + AccessibilityCheckResultType.ERROR -> roborazziErrorLog("Error: $check") + AccessibilityCheckResultType.WARNING -> roborazziErrorLog( + "Warning: $check" + ) + + AccessibilityCheckResultType.INFO -> roborazziReportLog( + "Info: $check" + ) + + else -> {} + } + } + + val failures = results.filter { it.type.ordinal <= failureLevel.ordinal } + if (failures.isNotEmpty()) { + throw AccessibilityViewCheckException(failures.toMutableList()) + } + + // TODO handle View cases +// } else if (captureRoot is CaptureRoot.View) { + } + } + + companion object +} + +data class AccessibilityChecksValidate( + val checker: ATFAccessibilityChecker +) : RoborazziRule.AccessibilityChecks { + override fun runAccessibilityChecks( + captureRoot: CaptureRoot, + roborazziOptions: RoborazziOptions + ) { + checker.runAccessibilityChecks( + captureRoot = captureRoot, + roborazziOptions = roborazziOptions, + ) + } +} diff --git a/roborazzi-accessibility-check/src/test/java/com/github/takahirom/roborazzi/ExampleUnitTest.kt b/roborazzi-accessibility-check/src/test/java/com/github/takahirom/roborazzi/ExampleUnitTest.kt new file mode 100644 index 000000000..426caaf20 --- /dev/null +++ b/roborazzi-accessibility-check/src/test/java/com/github/takahirom/roborazzi/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.github.takahirom.roborazzi + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/roborazzi-junit-rule/build.gradle b/roborazzi-junit-rule/build.gradle index 199055d78..7d453d29a 100644 --- a/roborazzi-junit-rule/build.gradle +++ b/roborazzi-junit-rule/build.gradle @@ -40,7 +40,6 @@ dependencies { implementation project(':roborazzi') implementation libs.androidx.test.ext.junit.ktx compileOnly libs.robolectric - compileOnly libs.androidx.test.ext.junit.ktx compileOnly libs.androidx.compose.ui.test compileOnly libs.androidx.compose.ui.test.junit4 api libs.accessibility.test.framework 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 fdae76987..f2d662346 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 @@ -1,13 +1,8 @@ package com.github.takahirom.roborazzi -import android.os.Build import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.test.espresso.ViewInteraction -import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType -import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck -import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult -import org.hamcrest.Matcher import org.junit.rules.TestWatcher import org.junit.runner.Description import org.junit.runners.model.Statement @@ -65,21 +60,20 @@ class RoborazziRule private constructor( ) @ExperimentalRoborazziApi - data class ATFAccessibilityChecker( - val checks: Set, - val suppressions: Matcher, - val failureLevel: AccessibilityCheckResultType, - ) { - companion object - } - - @ExperimentalRoborazziApi - sealed interface AccessibilityChecks { - data class Validate( - val checker: ATFAccessibilityChecker - ) : AccessibilityChecks - - data object Disabled : AccessibilityChecks + interface AccessibilityChecks { + fun runAccessibilityChecks( + captureRoot: CaptureRoot, + roborazziOptions: RoborazziOptions, + ) + // Use `roborazzi-accessibility-check`'s AccessibilityChecksValidate + data object Disabled : AccessibilityChecks { + override fun runAccessibilityChecks( + captureRoot: CaptureRoot, + roborazziOptions: RoborazziOptions + ) { + // Do nothing + } + } } sealed interface CaptureType { @@ -119,7 +113,8 @@ class RoborazziRule private constructor( ) : CaptureType } - internal sealed interface CaptureRoot { + @InternalRoborazziApi + sealed interface CaptureRoot { object None : CaptureRoot class Compose( val composeRule: ComposeTestRule, @@ -183,21 +178,16 @@ class RoborazziRule private constructor( ) { val evaluate: () -> Unit = { try { - val accessibilityValidator = ((options.accessibilityChecks as? AccessibilityChecks.Validate)?.checker) + val accessibilityChecks = options.accessibilityChecks // TODO enable a11y before showing content base.evaluate() - if (accessibilityValidator != null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - accessibilityValidator.runAccessibilityChecks( - captureRoot = captureRoot, - roborazziOptions = options.roborazziOptions - ) - } else { - roborazziErrorLog("Skipping accessibilityChecks on API " + Build.VERSION.SDK_INT + "(< ${Build.VERSION_CODES.UPSIDE_DOWN_CAKE})") - } - } + accessibilityChecks.runAccessibilityChecks( + captureRoot = captureRoot, + roborazziOptions = options.roborazziOptions + ) + } catch (e: Exception) { throw e } diff --git a/sample-android/build.gradle b/sample-android/build.gradle index 6a967196a..7f82c8842 100644 --- a/sample-android/build.gradle +++ b/sample-android/build.gradle @@ -64,6 +64,7 @@ dependencies { testImplementation(project(":roborazzi-ai-openai")) testImplementation project(":roborazzi-compose") testImplementation project(":roborazzi-junit-rule") + testImplementation project(":roborazzi-accessibility-check") implementation libs.androidx.compose.material3 implementation libs.androidx.compose.ui 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 4a7d89708..d5aebf4e2 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 @@ -20,12 +20,12 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.github.takahirom.roborazzi.ATFAccessibilityChecker +import com.github.takahirom.roborazzi.AccessibilityChecksValidate import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RoborazziOptions import com.github.takahirom.roborazzi.RoborazziOptions.RecordOptions import com.github.takahirom.roborazzi.RoborazziRule -import com.github.takahirom.roborazzi.RoborazziRule.ATFAccessibilityChecker -import com.github.takahirom.roborazzi.RoborazziRule.AccessibilityChecks import com.github.takahirom.roborazzi.RoborazziRule.CaptureType import com.github.takahirom.roborazzi.RoborazziRule.Options import com.github.takahirom.roborazzi.atf @@ -67,7 +67,7 @@ class ComposeA11yTest { applyDeviceCrop = true ), ), - accessibilityChecks = AccessibilityChecks.Validate( + accessibilityChecks = AccessibilityChecksValidate( checker = atfAccessibilityChecker ) ) diff --git a/settings.gradle b/settings.gradle index 76a15c9b9..9ce4c11f6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,6 +15,7 @@ dependencyResolutionManagement { rootProject.name = "roborazzi-root" include ':roborazzi' include ':roborazzi-junit-rule' +include ':roborazzi-accessibility-check' include ':roborazzi-compose-desktop' include ':roborazzi-compose-ios' include ':roborazzi-compose' From f35856eb8be442f3f7007a95d224638a54ac275f Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sun, 17 Nov 2024 10:45:05 +0000 Subject: [PATCH 06/34] Add docs and support LogOnly --- roborazzi-accessibility-check/README.md | 57 +++++++++++++++++++ .../com/github/takahirom/roborazzi/ATF.kt | 5 -- .../roborazzi/ATFAccessibilityChecker.kt | 21 ++++++- .../roborazzi/sample/ComposeA11yTest.kt | 22 ++----- 4 files changed, 81 insertions(+), 24 deletions(-) create mode 100644 roborazzi-accessibility-check/README.md diff --git a/roborazzi-accessibility-check/README.md b/roborazzi-accessibility-check/README.md new file mode 100644 index 000000000..a6425776b --- /dev/null +++ b/roborazzi-accessibility-check/README.md @@ -0,0 +1,57 @@ +# Roborazzi Acessibility Checks + +## How to use + +### Add dependencies + +| Description | Dependencies | +|---------------------|-----------------------------------------------------------------------------------------------| +| Accessibility Check | `testImplementation("io.github.takahirom.roborazzi:roborazzi-accessibility-check:[version]")` | + +### Configure in Junit Rule + +```kotlin + @get:Rule + val roborazziRule = RoborazziRule( + composeRule = composeTestRule, + captureRoot = composeTestRule.onRoot(), + options = Options( + accessibilityChecks = AccessibilityChecksValidate( + checker = ATFAccessibilityChecker.atf( + preset = AccessibilityCheckPreset.LATEST, + suppressions = matchesElements(withTestTag("suppress")) + ), + failureLevel = CheckLevel.Warning, + ) + ) + ) +``` + +### Checking Log output + +Particularly with `failureLevel = CheckLevel.LogOnly` the output log of each test will including a11y checks. + +```text +Error: [AccessibilityViewCheckResult check=AccessibilityHierarchyCheckResult ERROR SpeakableTextPresentCheck 4 [ViewHierarchyElement class=android.view.View bounds=Rect(474, 1074 - 606, 1206)] null num_answers:0 view=null] +Warning: [AccessibilityViewCheckResult check=AccessibilityHierarchyCheckResult WARNING TextContrastCheck 11 [ViewHierarchyElement class=android.widget.TextView text=Something hard to read bounds=Rect(403, 1002 - 678, 1092)] {KEY_BACKGROUND_COLOR=-12303292, KEY_CONTRAST_RATIO=1.02, KEY_FOREGROUND_COLOR=-12369085, KEY_REQUIRED_CONTRAST_RATIO=3.0} num_answers:0 view=null] +``` + +### LICENSE + +``` +Copyright 2023 takahirom +Copyright 2019 Square, Inc. +Copyright The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt index 95b9c448d..f0fec288b 100644 --- a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt @@ -4,7 +4,6 @@ import android.view.View import androidx.annotation.RequiresApi 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 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult import com.google.android.apps.common.testing.accessibility.framework.Parameters @@ -47,21 +46,17 @@ internal fun ATFAccessibilityChecker.runAllChecks( fun ATFAccessibilityChecker.Companion.atf( preset: AccessibilityCheckPreset? = AccessibilityCheckPreset.LATEST, suppressions: Matcher = Matchers.not(Matchers.anything()), - failureLevel: AccessibilityCheckResultType = AccessibilityCheckResultType.ERROR, ) = ATFAccessibilityChecker( checks = AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset(preset), suppressions = suppressions, - failureLevel = failureLevel ) fun ATFAccessibilityChecker.Companion.atf( checks: Set, suppressions: Matcher = Matchers.not(Matchers.anything()), - failureLevel: AccessibilityCheckResultType = AccessibilityCheckResultType.ERROR, ) = ATFAccessibilityChecker( checks = checks, suppressions = suppressions, - failureLevel = failureLevel ) diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt index 39031d821..fff73395d 100644 --- a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt @@ -15,12 +15,12 @@ import org.robolectric.shadows.ShadowBuild data class ATFAccessibilityChecker( val checks: Set, val suppressions: Matcher, - val failureLevel: AccessibilityCheckResultType, ) { @SuppressLint("VisibleForTests") fun runAccessibilityChecks( captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions, + failureLevel: CheckLevel, ) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { roborazziErrorLog("Skipping accessibilityChecks on API " + Build.VERSION.SDK_INT + "(< ${Build.VERSION_CODES.UPSIDE_DOWN_CAKE})") @@ -53,7 +53,7 @@ data class ATFAccessibilityChecker( } } - val failures = results.filter { it.type.ordinal <= failureLevel.ordinal } + val failures = results.filter { failureLevel.isFailure(it.type) } if (failures.isNotEmpty()) { throw AccessibilityViewCheckException(failures.toMutableList()) } @@ -66,8 +66,22 @@ data class ATFAccessibilityChecker( companion object } +enum class CheckLevel(private vararg val failedTypes: AccessibilityCheckResultType) { + Error(AccessibilityCheckResultType.ERROR), + + Warning( + AccessibilityCheckResultType.ERROR, + AccessibilityCheckResultType.WARNING + ), + + LogOnly; + + fun isFailure(type: AccessibilityCheckResultType): Boolean = failedTypes.contains(type) +} + data class AccessibilityChecksValidate( - val checker: ATFAccessibilityChecker + val checker: ATFAccessibilityChecker = ATFAccessibilityChecker.atf(), + val failureLevel: CheckLevel = CheckLevel.Error, ) : RoborazziRule.AccessibilityChecks { override fun runAccessibilityChecks( captureRoot: CaptureRoot, @@ -76,6 +90,7 @@ data class AccessibilityChecksValidate( checker.runAccessibilityChecks( captureRoot = captureRoot, roborazziOptions = roborazziOptions, + failureLevel = failureLevel, ) } } 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 d5aebf4e2..02e07b8d9 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 @@ -22,15 +22,12 @@ import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.ATFAccessibilityChecker import com.github.takahirom.roborazzi.AccessibilityChecksValidate +import com.github.takahirom.roborazzi.CheckLevel import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers -import com.github.takahirom.roborazzi.RoborazziOptions -import com.github.takahirom.roborazzi.RoborazziOptions.RecordOptions 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.atf 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.AccessibilityCheckResultUtils.matchesElements import com.google.android.apps.common.testing.accessibility.framework.matcher.ElementMatchers.withTestTag import org.junit.Rule @@ -51,24 +48,17 @@ class ComposeA11yTest { @get:Rule val composeTestRule = createAndroidComposeRule() - val atfAccessibilityChecker = ATFAccessibilityChecker.atf( - preset = AccessibilityCheckPreset.LATEST, - failureLevel = AccessibilityCheckResultType.WARNING, - suppressions = matchesElements(withTestTag("suppress")) - ) - @get:Rule val roborazziRule = RoborazziRule( composeRule = composeTestRule, captureRoot = composeTestRule.onRoot(), options = Options( - captureType = CaptureType.LastImage(), roborazziOptions = RoborazziOptions( - recordOptions = RecordOptions( - applyDeviceCrop = true - ), - ), accessibilityChecks = AccessibilityChecksValidate( - checker = atfAccessibilityChecker + checker = ATFAccessibilityChecker.atf( + preset = AccessibilityCheckPreset.LATEST, + suppressions = matchesElements(withTestTag("suppress")) + ), + failureLevel = CheckLevel.Warning, ) ) ) From 16c2b0aa2be7192f7b2f95bd65ec92d306d873ad Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sun, 17 Nov 2024 11:45:33 +0000 Subject: [PATCH 07/34] Demonstrate a custom check --- .../com/github/takahirom/roborazzi/ATF.kt | 10 +- .../roborazzi/ATFAccessibilityChecker.kt | 2 +- .../sample/ComposeA11yWithCustomCheckTest.kt | 181 ++++++++++++++++++ 3 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yWithCustomCheckTest.kt diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt index f0fec288b..144ede8b2 100644 --- a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt @@ -9,15 +9,16 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil import com.google.android.apps.common.testing.accessibility.framework.Parameters import com.google.android.apps.common.testing.accessibility.framework.ViewChecker import com.google.android.apps.common.testing.accessibility.framework.utils.contrast.BitmapImage +import com.google.common.collect.ImmutableSet import org.hamcrest.Matcher import org.hamcrest.Matchers - @RequiresApi(34) internal fun ATFAccessibilityChecker.runAllChecks( roborazziOptions: RoborazziOptions, view: View, - captureRoot: CaptureRoot.Compose + captureRoot: CaptureRoot.Compose, + checks: Set, ): List { val screenshot = RoboComponent.Compose(captureRoot.semanticsNodeInteraction.fetchSemanticsNode(), roborazziOptions).image @@ -33,10 +34,7 @@ internal fun ATFAccessibilityChecker.runAllChecks( setObtainCharacterLocations(true) } - val preset = AccessibilityCheckPreset.LATEST - val checks = AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset(preset) - - val results = viewChecker.runChecksOnView(checks, view, parameters) + val results = viewChecker.runChecksOnView(ImmutableSet.copyOf(checks), view, parameters) return results.filter { !suppressions.matches(it) diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt index fff73395d..da6f94a78 100644 --- a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt @@ -35,7 +35,7 @@ data class ATFAccessibilityChecker( (captureRoot.semanticsNodeInteraction.fetchSemanticsNode().root as ViewRootForTest).view.rootView // Will throw based on configuration - val results = runAllChecks(roborazziOptions, view, captureRoot) + val results = runAllChecks(roborazziOptions, view, captureRoot, checks) // Report on any warnings in the log output if not failing results.forEach { check -> 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 new file mode 100644 index 000000000..9bce394a9 --- /dev/null +++ b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ComposeA11yWithCustomCheckTest.kt @@ -0,0 +1,181 @@ +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.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material.Text +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.github.takahirom.roborazzi.ATFAccessibilityChecker +import com.github.takahirom.roborazzi.AccessibilityChecksValidate +import com.github.takahirom.roborazzi.CheckLevel +import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers +import com.github.takahirom.roborazzi.RoborazziRule +import com.github.takahirom.roborazzi.RoborazziRule.Options +import com.github.takahirom.roborazzi.atf +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 +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType.NOT_RUN +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesElements +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck +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.ResultMetadata +import com.google.android.apps.common.testing.accessibility.framework.matcher.ElementMatchers.withTestTag +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.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 +import java.util.Locale + +@RunWith(AndroidJUnit4::class) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@Config(qualifiers = RobolectricDeviceQualifiers.Pixel4, sdk = [35]) +class ComposeA11yWithCustomCheckTest { + @Suppress("DEPRECATION") + @get:Rule(order = Int.MIN_VALUE) + var thrown: ExpectedException = ExpectedException.none() + + @get:Rule + val composeTestRule = createAndroidComposeRule() + + @get:Rule + val roborazziRule = RoborazziRule( + composeRule = composeTestRule, + captureRoot = composeTestRule.onRoot(), + options = Options( + accessibilityChecks = AccessibilityChecksValidate( + checker = ATFAccessibilityChecker.atf( + checks = setOf(NoRedTextCheck()), + suppressions = matchesElements(withTestTag("suppress")) + ), + failureLevel = CheckLevel.Warning, + ) + ) + ) + + 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" + + override fun runCheckOnHierarchy( + hierarchy: AccessibilityHierarchy, + element: ViewHierarchyElement?, + parameters: Parameters? + ): List { + return getElementsToEvaluate(element, hierarchy).map { childElement -> + val mostRedTextColor: Color? = primaryTextColor(childElement, parameters) + + if (mostRedTextColor == null) { + result(childElement, NOT_RUN, 1) + } else if (mostRedTextColor.isMostlyRed()) { + result(childElement, ERROR, 3) + } else { + result(childElement, INFO, 3) + } + } + } + + private fun primaryTextColor( + childElement: ViewHierarchyElement, + parameters: Parameters? + ) = if (childElement.text == null) { + null + } else if (childElement.isVisibleToUser != true) { + null + } else { + val textColor = childElement.textColor + if (textColor != null) { + Color(textColor) + } else { + val screenCapture = parameters?.screenCapture + val textCharacterLocations = childElement.textCharacterLocations + + if (screenCapture == null || textCharacterLocations.isEmpty()) { + null + } else { + val argb = textCharacterLocations.firstNotNullOfOrNull { rect -> + screenCapture.crop(rect.left, rect.top, rect.width, rect.height).pixels.firstOrNull { + Color(it).isMostlyRed() + } + } + if (argb != null) Color(argb) else null + } + } + } + + private fun Color.isMostlyRed(): Boolean { + return red > 0.8f && blue < 0.2f && green < 0.2f + } + + private fun result( + childElement: ViewHierarchyElement?, + result: AccessibilityCheckResultType, + resultId: Int + ) = CustomAccessibilityHierarchyCheckResult( + this::class.java, + result, + childElement, + resultId, + null + ) + } + + // 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") + + composeTestRule.setContent { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Box(modifier = Modifier.size(100.dp).background(Color.DarkGray)) { + Text("Something red and inappropriate", color = Color.Red) + } + } + } + } + + @Test + fun normalText() { + composeTestRule.setContent { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Box(modifier = Modifier.size(100.dp).background(Color.White)) { + Text("Something boring and black", color = Color.Black) + } + } + } + } +} + From aa8513a1dd34ab55f988d45a637165f8571b95c5 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sun, 17 Nov 2024 12:06:22 +0000 Subject: [PATCH 08/34] Demonstrate a custom check --- .../sample/ComposeA11yWithCustomCheckTest.kt | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) 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 9bce394a9..d52f81e47 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 @@ -9,6 +9,7 @@ import androidx.compose.material.Text import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onRoot import androidx.compose.ui.unit.dp @@ -27,6 +28,7 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesElements import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult +import com.google.android.apps.common.testing.accessibility.framework.HashMapResultMetadata import com.google.android.apps.common.testing.accessibility.framework.Parameters import com.google.android.apps.common.testing.accessibility.framework.ResultMetadata import com.google.android.apps.common.testing.accessibility.framework.matcher.ElementMatchers.withTestTag @@ -85,29 +87,29 @@ class ComposeA11yWithCustomCheckTest { parameters: Parameters? ): List { return getElementsToEvaluate(element, hierarchy).map { childElement -> - val mostRedTextColor: Color? = primaryTextColor(childElement, parameters) + val textColors = primaryTextColors(childElement, parameters) - if (mostRedTextColor == null) { - result(childElement, NOT_RUN, 1) - } else if (mostRedTextColor.isMostlyRed()) { - result(childElement, ERROR, 3) + if (textColors == null) { + result(childElement, NOT_RUN, 1, null) + } else if (textColors.find { it.isMostlyRed() } != null) { + result(childElement, ERROR, 3, textColors) } else { - result(childElement, INFO, 3) + result(childElement, INFO, 3, textColors) } } } - private fun primaryTextColor( + private fun primaryTextColors( childElement: ViewHierarchyElement, parameters: Parameters? - ) = if (childElement.text == null) { + ): Set? = if (childElement.text == null) { null } else if (childElement.isVisibleToUser != true) { null } else { val textColor = childElement.textColor if (textColor != null) { - Color(textColor) + setOf(Color(textColor)) } else { val screenCapture = parameters?.screenCapture val textCharacterLocations = childElement.textCharacterLocations @@ -115,12 +117,9 @@ class ComposeA11yWithCustomCheckTest { if (screenCapture == null || textCharacterLocations.isEmpty()) { null } else { - val argb = textCharacterLocations.firstNotNullOfOrNull { rect -> - screenCapture.crop(rect.left, rect.top, rect.width, rect.height).pixels.firstOrNull { - Color(it).isMostlyRed() - } - } - if (argb != null) Color(argb) else null + textCharacterLocations.flatMap { rect -> + screenCapture.crop(rect.left, rect.top, rect.width, rect.height).pixels.asSequence() + }.distinct().map { Color(it) }.toSet() } } } @@ -132,13 +131,18 @@ class ComposeA11yWithCustomCheckTest { private fun result( childElement: ViewHierarchyElement?, result: AccessibilityCheckResultType, - resultId: Int + resultId: Int, + textColors: Iterable? ) = CustomAccessibilityHierarchyCheckResult( this::class.java, result, childElement, resultId, - null + HashMapResultMetadata().apply { + if (textColors != null) { + putString("textColors", textColors.joinToString { "0x${it.toArgb().toUInt().toString(16)}" }) + } + } ) } From 59e9328c96fea876e70ec1269768d7da09bf63f2 Mon Sep 17 00:00:00 2001 From: takahirom Date: Sun, 17 Nov 2024 21:44:07 +0900 Subject: [PATCH 09/34] Remove libs.accessibility.test.framework from roborazzi-junit-rule --- roborazzi-junit-rule/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/roborazzi-junit-rule/build.gradle b/roborazzi-junit-rule/build.gradle index 7d453d29a..ae3504a60 100644 --- a/roborazzi-junit-rule/build.gradle +++ b/roborazzi-junit-rule/build.gradle @@ -42,5 +42,4 @@ dependencies { compileOnly libs.robolectric compileOnly libs.androidx.compose.ui.test compileOnly libs.androidx.compose.ui.test.junit4 - api libs.accessibility.test.framework } \ No newline at end of file From e5036ee783b98a5c0f9a9f1a5b757ecc3cc3f221 Mon Sep 17 00:00:00 2001 From: takahirom Date: Sun, 17 Nov 2024 22:01:52 +0900 Subject: [PATCH 10/34] Remove unneeded dependencies from roborazzi-accessibility-check --- roborazzi-accessibility-check/build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/roborazzi-accessibility-check/build.gradle b/roborazzi-accessibility-check/build.gradle index 1c737358b..c50af0ef5 100644 --- a/roborazzi-accessibility-check/build.gradle +++ b/roborazzi-accessibility-check/build.gradle @@ -36,9 +36,7 @@ android { dependencies { implementation project(':roborazzi-junit-rule') implementation project(':roborazzi') - implementation libs.androidx.test.ext.junit.ktx compileOnly libs.robolectric compileOnly libs.androidx.compose.ui.test - compileOnly libs.androidx.compose.ui.test.junit4 api libs.accessibility.test.framework } \ No newline at end of file From 23f2be933b8f51e574410023446db0bce52c5904 Mon Sep 17 00:00:00 2001 From: takahirom Date: Sun, 17 Nov 2024 22:26:14 +0900 Subject: [PATCH 11/34] Support View Accessibility checks --- .../com/github/takahirom/roborazzi/ATF.kt | 12 +-- .../roborazzi/ATFAccessibilityChecker.kt | 79 ++++++++++++++----- 2 files changed, 64 insertions(+), 27 deletions(-) diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt index 144ede8b2..ecb5d3c72 100644 --- a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt @@ -1,8 +1,8 @@ package com.github.takahirom.roborazzi +import android.graphics.Bitmap import android.view.View import androidx.annotation.RequiresApi -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.AccessibilityHierarchyCheck import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult @@ -15,17 +15,13 @@ import org.hamcrest.Matchers @RequiresApi(34) internal fun ATFAccessibilityChecker.runAllChecks( - roborazziOptions: RoborazziOptions, view: View, - captureRoot: CaptureRoot.Compose, + screenshotBitmap: Bitmap?, checks: Set, ): List { - val screenshot = - RoboComponent.Compose(captureRoot.semanticsNodeInteraction.fetchSemanticsNode(), roborazziOptions).image - val parameters = Parameters().apply { - if (screenshot != null) { - putScreenCapture(BitmapImage(screenshot)) + if (screenshotBitmap != null) { + putScreenCapture(BitmapImage(screenshotBitmap)) } setSaveViewImages(true) } diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt index da6f94a78..68d2ba877 100644 --- a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt @@ -1,14 +1,19 @@ package com.github.takahirom.roborazzi import android.annotation.SuppressLint +import android.graphics.Bitmap import android.os.Build +import android.view.View import androidx.compose.ui.platform.ViewRootForTest +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction import com.github.takahirom.roborazzi.RoborazziRule.CaptureRoot import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult import com.google.android.apps.common.testing.accessibility.framework.integrations.espresso.AccessibilityViewCheckException import org.hamcrest.Matcher +import org.hamcrest.Matchers import org.robolectric.shadows.ShadowBuild @ExperimentalRoborazziApi @@ -35,31 +40,67 @@ data class ATFAccessibilityChecker( (captureRoot.semanticsNodeInteraction.fetchSemanticsNode().root as ViewRootForTest).view.rootView // Will throw based on configuration - val results = runAllChecks(roborazziOptions, view, captureRoot, checks) - - // Report on any warnings in the log output if not failing - results.forEach { check -> - when (check.type) { - AccessibilityCheckResultType.ERROR -> roborazziErrorLog("Error: $check") - AccessibilityCheckResultType.WARNING -> roborazziErrorLog( - "Warning: $check" - ) - AccessibilityCheckResultType.INFO -> roborazziReportLog( - "Info: $check" - ) + val screenshot: Bitmap? = + RoboComponent.Compose( + node = captureRoot.semanticsNodeInteraction.fetchSemanticsNode(), + roborazziOptions = roborazziOptions + ).image + + val results = runAllChecks(view, screenshot, checks) + + reportResults(results, failureLevel) - else -> {} + } else if (captureRoot is CaptureRoot.View) { + val viewInteraction = captureRoot.viewInteraction + // Use perform to get the view + viewInteraction.perform(object : ViewAction { + override fun getDescription(): String { + return "Run accessibility checks" } - } - val failures = results.filter { failureLevel.isFailure(it.type) } - if (failures.isNotEmpty()) { - throw AccessibilityViewCheckException(failures.toMutableList()) + override fun getConstraints(): Matcher { + return Matchers.any(View::class.java) + } + + override fun perform(uiController: UiController?, view: View?) { + if (view == null) { + throw IllegalStateException("View is null") + } + val results = runAllChecks( + view = view, + screenshotBitmap = RoboComponent.View(view, roborazziOptions).image, + checks = checks + ) + reportResults(results, failureLevel) + } + }) + } + } + + private fun reportResults( + results: List, + failureLevel: CheckLevel + ) { + // Report on any warnings in the log output if not failing + results.forEach { check -> + when (check.type) { + AccessibilityCheckResultType.ERROR -> roborazziErrorLog("Error: $check") + AccessibilityCheckResultType.WARNING -> roborazziErrorLog( + "Warning: $check" + ) + + AccessibilityCheckResultType.INFO -> roborazziReportLog( + "Info: $check" + ) + + else -> {} } + } - // TODO handle View cases -// } else if (captureRoot is CaptureRoot.View) { + val failures = results.filter { failureLevel.isFailure(it.type) } + if (failures.isNotEmpty()) { + throw AccessibilityViewCheckException(failures.toMutableList()) } } From a1725506901c2260e8f98ab28011c36784d32d6d Mon Sep 17 00:00:00 2001 From: takahirom Date: Sun, 17 Nov 2024 22:29:40 +0900 Subject: [PATCH 12/34] Add ExperimentalRoborazziApi to some APIs --- .../com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt index 68d2ba877..751140d3a 100644 --- a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt @@ -107,6 +107,7 @@ data class ATFAccessibilityChecker( companion object } +@ExperimentalRoborazziApi enum class CheckLevel(private vararg val failedTypes: AccessibilityCheckResultType) { Error(AccessibilityCheckResultType.ERROR), @@ -120,6 +121,7 @@ enum class CheckLevel(private vararg val failedTypes: AccessibilityCheckResultTy fun isFailure(type: AccessibilityCheckResultType): Boolean = failedTypes.contains(type) } +@ExperimentalRoborazziApi data class AccessibilityChecksValidate( val checker: ATFAccessibilityChecker = ATFAccessibilityChecker.atf(), val failureLevel: CheckLevel = CheckLevel.Error, From 21c16c36cc2247d9b52973f8e1fd03534a669479 Mon Sep 17 00:00:00 2001 From: takahirom Date: Sun, 17 Nov 2024 22:35:37 +0900 Subject: [PATCH 13/34] Make runAllChecks normal function --- .../main/java/com/github/takahirom/roborazzi/ATF.kt | 3 ++- .../takahirom/roborazzi/ATFAccessibilityChecker.kt | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt index ecb5d3c72..21b9b5d19 100644 --- a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt @@ -14,10 +14,11 @@ import org.hamcrest.Matcher import org.hamcrest.Matchers @RequiresApi(34) -internal fun ATFAccessibilityChecker.runAllChecks( +internal fun runAllChecks( view: View, screenshotBitmap: Bitmap?, checks: Set, + suppressions: Matcher, ): List { val parameters = Parameters().apply { if (screenshotBitmap != null) { diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt index 751140d3a..6260815d5 100644 --- a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt @@ -47,7 +47,12 @@ data class ATFAccessibilityChecker( roborazziOptions = roborazziOptions ).image - val results = runAllChecks(view, screenshot, checks) + val results = runAllChecks( + view = view, + screenshotBitmap = screenshot, + checks = checks, + suppressions = suppressions + ) reportResults(results, failureLevel) @@ -70,7 +75,8 @@ data class ATFAccessibilityChecker( val results = runAllChecks( view = view, screenshotBitmap = RoboComponent.View(view, roborazziOptions).image, - checks = checks + checks = checks, + suppressions = suppressions ) reportResults(results, failureLevel) } From ae3b04c03399939476c1adb68d82117191dd8377 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sun, 17 Nov 2024 14:37:27 +0000 Subject: [PATCH 14/34] Cleanup --- roborazzi-accessibility-check/README.md | 2 +- .../com/github/takahirom/roborazzi/ATF.kt | 57 ------------- .../roborazzi/ATFAccessibilityChecker.kt | 82 +++++++++++++------ .../roborazzi/sample/ComposeA11yTest.kt | 3 +- .../sample/ComposeA11yWithCustomCheckTest.kt | 3 +- 5 files changed, 62 insertions(+), 85 deletions(-) delete mode 100644 roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt diff --git a/roborazzi-accessibility-check/README.md b/roborazzi-accessibility-check/README.md index a6425776b..f86c53a3c 100644 --- a/roborazzi-accessibility-check/README.md +++ b/roborazzi-accessibility-check/README.md @@ -17,7 +17,7 @@ captureRoot = composeTestRule.onRoot(), options = Options( accessibilityChecks = AccessibilityChecksValidate( - checker = ATFAccessibilityChecker.atf( + checker = ATFAccessibilityChecker( preset = AccessibilityCheckPreset.LATEST, suppressions = matchesElements(withTestTag("suppress")) ), diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt deleted file mode 100644 index 21b9b5d19..000000000 --- a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATF.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.github.takahirom.roborazzi - -import android.graphics.Bitmap -import android.view.View -import androidx.annotation.RequiresApi -import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset -import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck -import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult -import com.google.android.apps.common.testing.accessibility.framework.Parameters -import com.google.android.apps.common.testing.accessibility.framework.ViewChecker -import com.google.android.apps.common.testing.accessibility.framework.utils.contrast.BitmapImage -import com.google.common.collect.ImmutableSet -import org.hamcrest.Matcher -import org.hamcrest.Matchers - -@RequiresApi(34) -internal fun runAllChecks( - view: View, - screenshotBitmap: Bitmap?, - checks: Set, - suppressions: Matcher, -): List { - val parameters = Parameters().apply { - if (screenshotBitmap != null) { - putScreenCapture(BitmapImage(screenshotBitmap)) - } - setSaveViewImages(true) - } - - val viewChecker = ViewChecker().apply { - setObtainCharacterLocations(true) - } - - val results = viewChecker.runChecksOnView(ImmutableSet.copyOf(checks), view, parameters) - - return results.filter { - !suppressions.matches(it) - } -} - -fun ATFAccessibilityChecker.Companion.atf( - preset: AccessibilityCheckPreset? = AccessibilityCheckPreset.LATEST, - suppressions: Matcher = Matchers.not(Matchers.anything()), -) = - ATFAccessibilityChecker( - checks = AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset(preset), - suppressions = suppressions, - ) - -fun ATFAccessibilityChecker.Companion.atf( - checks: Set, - suppressions: Matcher = Matchers.not(Matchers.anything()), -) = - ATFAccessibilityChecker( - checks = checks, - suppressions = suppressions, - ) diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt index 6260815d5..d7b870a81 100644 --- a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt @@ -4,23 +4,65 @@ import android.annotation.SuppressLint import android.graphics.Bitmap import android.os.Build import android.view.View +import androidx.annotation.RequiresApi import androidx.compose.ui.platform.ViewRootForTest import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction 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 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult +import com.google.android.apps.common.testing.accessibility.framework.Parameters +import com.google.android.apps.common.testing.accessibility.framework.ViewChecker import com.google.android.apps.common.testing.accessibility.framework.integrations.espresso.AccessibilityViewCheckException +import com.google.android.apps.common.testing.accessibility.framework.utils.contrast.BitmapImage +import com.google.common.collect.ImmutableSet import org.hamcrest.Matcher import org.hamcrest.Matchers import org.robolectric.shadows.ShadowBuild @ExperimentalRoborazziApi data class ATFAccessibilityChecker( - val checks: Set, - val suppressions: Matcher, + val checks: Set = AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset( + AccessibilityCheckPreset.LATEST + ), + val suppressions: Matcher = Matchers.not(Matchers.anything()), ) { + constructor( + preset: AccessibilityCheckPreset, + suppressions: Matcher = Matchers.not(Matchers.anything()), + ) : this( + checks = AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset(preset), + suppressions = suppressions, + ) + + + @RequiresApi(34) + internal fun runAllChecks( + view: View, + screenshotBitmap: Bitmap?, + checks: Set, + suppressions: Matcher, + ): List { + val parameters = Parameters().apply { + if (screenshotBitmap != null) { + putScreenCapture(BitmapImage(screenshotBitmap)) + } + setSaveViewImages(true) + } + + val viewChecker = ViewChecker().apply { + setObtainCharacterLocations(true) + } + + val results = viewChecker.runChecksOnView(ImmutableSet.copyOf(checks), view, parameters) + + return results.filter { + !suppressions.matches(it) + } + } + @SuppressLint("VisibleForTests") fun runAccessibilityChecks( captureRoot: CaptureRoot, @@ -31,27 +73,24 @@ data class ATFAccessibilityChecker( roborazziErrorLog("Skipping accessibilityChecks on API " + Build.VERSION.SDK_INT + "(< ${Build.VERSION_CODES.UPSIDE_DOWN_CAKE})") return } - // TODO remove this once ATF doesn't bail out - // https://github.com/google/Accessibility-Test-Framework-for-Android/blob/c65cab02b2a845c29c3da100d6adefd345a144e3/src/main/java/com/google/android/apps/common/testing/accessibility/framework/uielement/AccessibilityHierarchyAndroid.java#L667 - ShadowBuild.setFingerprint("roborazzi") + + if (Build.FINGERPRINT == "robolectric") { + // TODO remove this once ATF doesn't bail out + // https://github.com/google/Accessibility-Test-Framework-for-Android/blob/c65cab02b2a845c29c3da100d6adefd345a144e3/src/main/java/com/google/android/apps/common/testing/accessibility/framework/uielement/AccessibilityHierarchyAndroid.java#L667 + ShadowBuild.setFingerprint("roborazzi") + } if (captureRoot is CaptureRoot.Compose) { - val view = - (captureRoot.semanticsNodeInteraction.fetchSemanticsNode().root as ViewRootForTest).view.rootView + val view = (captureRoot.semanticsNodeInteraction.fetchSemanticsNode().root as ViewRootForTest).view.rootView // Will throw based on configuration - val screenshot: Bitmap? = - RoboComponent.Compose( - node = captureRoot.semanticsNodeInteraction.fetchSemanticsNode(), - roborazziOptions = roborazziOptions - ).image + val screenshot: Bitmap? = RoboComponent.Compose( + node = captureRoot.semanticsNodeInteraction.fetchSemanticsNode(), roborazziOptions = roborazziOptions + ).image val results = runAllChecks( - view = view, - screenshotBitmap = screenshot, - checks = checks, - suppressions = suppressions + view = view, screenshotBitmap = screenshot, checks = checks, suppressions = suppressions ) reportResults(results, failureLevel) @@ -85,8 +124,7 @@ data class ATFAccessibilityChecker( } private fun reportResults( - results: List, - failureLevel: CheckLevel + results: List, failureLevel: CheckLevel ) { // Report on any warnings in the log output if not failing results.forEach { check -> @@ -118,8 +156,7 @@ enum class CheckLevel(private vararg val failedTypes: AccessibilityCheckResultTy Error(AccessibilityCheckResultType.ERROR), Warning( - AccessibilityCheckResultType.ERROR, - AccessibilityCheckResultType.WARNING + AccessibilityCheckResultType.ERROR, AccessibilityCheckResultType.WARNING ), LogOnly; @@ -129,12 +166,11 @@ enum class CheckLevel(private vararg val failedTypes: AccessibilityCheckResultTy @ExperimentalRoborazziApi data class AccessibilityChecksValidate( - val checker: ATFAccessibilityChecker = ATFAccessibilityChecker.atf(), + val checker: ATFAccessibilityChecker = ATFAccessibilityChecker(), val failureLevel: CheckLevel = CheckLevel.Error, ) : RoborazziRule.AccessibilityChecks { override fun runAccessibilityChecks( - captureRoot: CaptureRoot, - roborazziOptions: RoborazziOptions + captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions ) { checker.runAccessibilityChecks( captureRoot = captureRoot, 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 02e07b8d9..c5f68b364 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 @@ -26,7 +26,6 @@ import com.github.takahirom.roborazzi.CheckLevel import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RoborazziRule import com.github.takahirom.roborazzi.RoborazziRule.Options -import com.github.takahirom.roborazzi.atf 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 @@ -54,7 +53,7 @@ class ComposeA11yTest { captureRoot = composeTestRule.onRoot(), options = Options( accessibilityChecks = AccessibilityChecksValidate( - checker = ATFAccessibilityChecker.atf( + checker = ATFAccessibilityChecker( preset = AccessibilityCheckPreset.LATEST, suppressions = matchesElements(withTestTag("suppress")) ), 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 d52f81e47..1dd3f4be1 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 @@ -20,7 +20,6 @@ import com.github.takahirom.roborazzi.CheckLevel import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RoborazziRule import com.github.takahirom.roborazzi.RoborazziRule.Options -import com.github.takahirom.roborazzi.atf 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 @@ -59,7 +58,7 @@ class ComposeA11yWithCustomCheckTest { captureRoot = composeTestRule.onRoot(), options = Options( accessibilityChecks = AccessibilityChecksValidate( - checker = ATFAccessibilityChecker.atf( + checker = ATFAccessibilityChecker( checks = setOf(NoRedTextCheck()), suppressions = matchesElements(withTestTag("suppress")) ), From 8f18eadb63175d1732343cf219c87032faac2904 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sun, 17 Nov 2024 14:39:43 +0000 Subject: [PATCH 15/34] Explain the custom check --- .../roborazzi/sample/ComposeA11yWithCustomCheckTest.kt | 8 ++++++++ 1 file changed, 8 insertions(+) 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 1dd3f4be1..405b5fb6c 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 @@ -41,6 +41,11 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode import java.util.Locale +/** + * 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]) @@ -67,6 +72,9 @@ class ComposeA11yWithCustomCheckTest { ) ) + /** + * Custom Check that demonstrates accessing the screenshot and element data. + */ class NoRedTextCheck : AccessibilityHierarchyCheck() { override fun getHelpTopic(): String? = null From f1ac5b33f2a382089bd8e72c07e9aed1e2a84d66 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sun, 17 Nov 2024 14:41:21 +0000 Subject: [PATCH 16/34] Cleanup --- .../roborazzi/sample/ComposeA11yWithCustomCheckTest.kt | 2 -- 1 file changed, 2 deletions(-) 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 405b5fb6c..d1ad3f361 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 @@ -43,8 +43,6 @@ import java.util.Locale /** * Test demonstrating a completely custom ATF Check. Expected to be a niche usecase, but critical when required. - * - * */ @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) From 940aea666095b5c0ffbfb4a708ad6015efc8e33e Mon Sep 17 00:00:00 2001 From: takahirom Date: Mon, 18 Nov 2024 10:38:16 +0900 Subject: [PATCH 17/34] Add View test --- .../roborazzi/sample/ComposeA11yTest.kt | 2 +- .../roborazzi/sample/ViewA11yTest.kt | 173 ++++++++++++++++++ 2 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ViewA11yTest.kt 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 c5f68b364..1f31bfb2c 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 @@ -144,7 +144,7 @@ class ComposeA11yTest { composeTestRule.setContent { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.size(100.dp).background(Color.DarkGray)) { - Text("Something hard to read", color = Color.White) + Text("Something not hard to read", color = Color.White) } } } 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 new file mode 100644 index 000000000..6d8396bfe --- /dev/null +++ b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ViewA11yTest.kt @@ -0,0 +1,173 @@ +package com.github.takahirom.roborazzi.sample + +import android.graphics.Color +import android.view.View +import android.widget.FrameLayout +import android.widget.TextView +import androidx.activity.ComponentActivity +import androidx.test.espresso.Espresso +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.github.takahirom.roborazzi.ATFAccessibilityChecker +import com.github.takahirom.roborazzi.AccessibilityChecksValidate +import com.github.takahirom.roborazzi.CheckLevel +import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers +import com.github.takahirom.roborazzi.RoborazziRule +import com.github.takahirom.roborazzi.RoborazziRule.Options +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 +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 + +@RunWith(AndroidJUnit4::class) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@Config(qualifiers = RobolectricDeviceQualifiers.Pixel4, sdk = [35]) +class ViewA11yTest { + @Suppress("DEPRECATION") + @get:Rule(order = Int.MIN_VALUE) + var thrown: ExpectedException = ExpectedException.none() + + @get:Rule + val activityScenarioRule = ActivityScenarioRule(ComponentActivity::class.java) + + @get:Rule + val roborazziRule = RoborazziRule( + captureRoot = Espresso.onView(ViewMatchers.isRoot()), + options = Options( + accessibilityChecks = AccessibilityChecksValidate( + checker = ATFAccessibilityChecker( + preset = AccessibilityCheckPreset.LATEST, + suppressions = matchesElements(withTestTag("suppress")) + ), + failureLevel = CheckLevel.Warning, + ) + ) + ) + + @Test + fun clickableWithoutSemantics() { + thrown.expectMessage("SpeakableTextPresentCheck") + + activityScenarioRule.scenario.onActivity { activity -> + activity.setContentView( + FrameLayout(activity).apply { + id = android.R.id.content + addView( + View(activity).apply { + setBackgroundColor(Color.BLACK) + setOnClickListener { } + } + ) + } + ) + } + } + + @Test + fun smallClickable() { + // for(ViewHierarchyElement view : getElementsToEvaluate(fromRoot, hierarchy)) { + // if (!Boolean.TRUE.equals(view.isClickable()) && !Boolean.TRUE.equals(view.isLongClickable())) { + // TODO investigate +// thrown.expectMessage("TouchTargetSizeCheck") + + activityScenarioRule.scenario.onActivity { activity -> + activity.setContentView( + FrameLayout(activity).apply { + id = android.R.id.content + addView( + View(activity).apply { + contentDescription = "clickable" + setBackgroundColor(Color.BLACK) + setOnClickListener { } + } + ) + } + ) + } + onView(ViewMatchers.withContentDescription("clickable")).check(ViewAssertions.matches(ViewMatchers.isClickable())) + } + + @Test + fun clickableBox() { + activityScenarioRule.scenario.onActivity { activity -> + activity.setContentView( + FrameLayout(activity).apply { + id = android.R.id.content + setBackgroundColor(Color.WHITE) + addView( + TextView(activity).apply { + text = "Something to Click" + setTextColor(Color.BLACK) + setOnClickListener { } + } + ) + } + ) + } + } + + @Test + fun supressionsTakeEffect() { + activityScenarioRule.scenario.onActivity { activity -> + activity.setContentView( + FrameLayout(activity).apply { + id = android.R.id.content + addView( + View(activity).apply { + setBackgroundColor(Color.BLACK) + contentDescription = "" + tag = "suppress" + } + ) + } + ) + } + } + + @Test + fun faintText() { + thrown.expectMessage("TextContrastCheck") + + activityScenarioRule.scenario.onActivity { activity -> + activity.setContentView( + FrameLayout(activity).apply { + id = android.R.id.content + setBackgroundColor(Color.BLACK) + addView( + TextView(activity).apply { + text = "Something hard to read" + setTextColor(Color.DKGRAY) + } + ) + } + ) + } + } + + @Test + fun normalText() { + activityScenarioRule.scenario.onActivity { activity -> + activity.setContentView( + FrameLayout(activity).apply { + id = android.R.id.content + setBackgroundColor(Color.DKGRAY) + addView( + TextView(activity).apply { + text = "Something hard to read" + setTextColor(Color.WHITE) + } + ) + } + ) + } + } +} + From 4ba0a44d324a0e291dfbb223df6e3fde30ffdbc2 Mon Sep 17 00:00:00 2001 From: takahirom Date: Mon, 18 Nov 2024 11:18:20 +0900 Subject: [PATCH 18/34] Fix API visibility --- .../com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt | 4 +--- .../main/java/com/github/takahirom/roborazzi/RoborazziRule.kt | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt index d7b870a81..4d0810278 100644 --- a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt @@ -1,6 +1,5 @@ package com.github.takahirom.roborazzi -import android.annotation.SuppressLint import android.graphics.Bitmap import android.os.Build import android.view.View @@ -63,8 +62,7 @@ data class ATFAccessibilityChecker( } } - @SuppressLint("VisibleForTests") - fun runAccessibilityChecks( + internal fun runAccessibilityChecks( captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions, failureLevel: CheckLevel, 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 f2d662346..d6bc15d4d 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 @@ -61,6 +61,7 @@ class RoborazziRule private constructor( @ExperimentalRoborazziApi interface AccessibilityChecks { + @InternalRoborazziApi fun runAccessibilityChecks( captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions, From c526a25ffcc022457b3057de3d657ce80443a31f Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Mon, 18 Nov 2024 10:38:38 +0000 Subject: [PATCH 19/34] Demonstrate running on a composable --- .../roborazzi/ATFAccessibilityChecker.kt | 4 +- .../roborazzi/sample/ComposeA11yTest.kt | 96 ++++++++++++++++--- 2 files changed, 88 insertions(+), 12 deletions(-) diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt index 4d0810278..eeff70915 100644 --- a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt @@ -1,5 +1,6 @@ package com.github.takahirom.roborazzi +import android.annotation.SuppressLint import android.graphics.Bitmap import android.os.Build import android.view.View @@ -62,6 +63,7 @@ data class ATFAccessibilityChecker( } } + @SuppressLint("VisibleForTests") internal fun runAccessibilityChecks( captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions, @@ -79,7 +81,7 @@ data class ATFAccessibilityChecker( } if (captureRoot is CaptureRoot.Compose) { - val view = (captureRoot.semanticsNodeInteraction.fetchSemanticsNode().root as ViewRootForTest).view.rootView + val view = (captureRoot.semanticsNodeInteraction.fetchSemanticsNode().root as ViewRootForTest).view // Will throw based on configuration 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 1f31bfb2c..9dbae80c7 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 @@ -3,7 +3,9 @@ package com.github.takahirom.roborazzi.sample import androidx.activity.ComponentActivity import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement 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 @@ -24,6 +26,7 @@ import com.github.takahirom.roborazzi.ATFAccessibilityChecker import com.github.takahirom.roborazzi.AccessibilityChecksValidate import com.github.takahirom.roborazzi.CheckLevel import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers +import com.github.takahirom.roborazzi.RoborazziOptions import com.github.takahirom.roborazzi.RoborazziRule import com.github.takahirom.roborazzi.RoborazziRule.Options import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset @@ -57,7 +60,7 @@ class ComposeA11yTest { preset = AccessibilityCheckPreset.LATEST, suppressions = matchesElements(withTestTag("suppress")) ), - failureLevel = CheckLevel.Warning, + failureLevel = CheckLevel.LogOnly, ) ) ) @@ -68,7 +71,11 @@ class ComposeA11yTest { composeTestRule.setContent { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Box(Modifier.size(48.dp).background(Color.Black).clickable {}) + Box( + Modifier + .size(48.dp) + .background(Color.Black) + .clickable {}) } } } @@ -79,9 +86,13 @@ class ComposeA11yTest { composeTestRule.setContent { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Box(Modifier.size(48.dp).background(Color.Black).semantics { - contentDescription = "" - }) + Box( + Modifier + .size(48.dp) + .background(Color.Black) + .semantics { + contentDescription = "" + }) } } } @@ -95,7 +106,11 @@ class ComposeA11yTest { composeTestRule.setContent { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Button(onClick = {}, modifier = Modifier.size(30.dp).testTag("clickable")) { + Button( + onClick = {}, modifier = Modifier + .size(30.dp) + .testTag("clickable") + ) { Text("Something to Click") } } @@ -119,9 +134,14 @@ class ComposeA11yTest { fun supressionsTakeEffect() { composeTestRule.setContent { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Box(Modifier.size(48.dp).background(Color.Black).testTag("suppress").semantics { - contentDescription = "" - }) + Box( + Modifier + .size(48.dp) + .background(Color.Black) + .testTag("suppress") + .semantics { + contentDescription = "" + }) } } } @@ -132,7 +152,11 @@ class ComposeA11yTest { composeTestRule.setContent { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Box(modifier = Modifier.size(100.dp).background(Color.DarkGray)) { + Box( + modifier = Modifier + .size(100.dp) + .background(Color.DarkGray) + ) { Text("Something hard to read", color = Color.DarkGray) } } @@ -143,11 +167,61 @@ class ComposeA11yTest { fun normalText() { composeTestRule.setContent { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Box(modifier = Modifier.size(100.dp).background(Color.DarkGray)) { + Box( + modifier = Modifier + .size(100.dp) + .background(Color.DarkGray) + ) { Text("Something not hard to read", color = Color.White) } } } } + + @Test + fun composableOnly() { + composeTestRule.setContent { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Box( + modifier = Modifier + .size(100.dp) + .background(Color.DarkGray) + .testTag("nothard") + ) { + Text("Something not hard to read", color = Color.White) + } + Box( + modifier = Modifier + .size(100.dp) + .background(Color.DarkGray) + .testTag("suppress") + ) { + Text("Something hard to read", color = Color.DarkGray) + } + } + } + + // Now run without suppressions + val a11yChecks = AccessibilityChecksValidate( + checker = ATFAccessibilityChecker( + preset = AccessibilityCheckPreset.LATEST, + ), + failureLevel = CheckLevel.Warning, + ) + + // Run only against nothard, shouldn't fail because of the hard to read text + a11yChecks + .runAccessibilityChecks( + captureRoot = RoborazziRule.CaptureRoot.Compose( + composeTestRule, + composeTestRule.onNodeWithTag("nothard"), + ), + roborazziOptions = RoborazziOptions() + ) + } } From f5145558ff92e5b5b73e6131a3c28e3a89e6f361 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Mon, 18 Nov 2024 11:45:23 +0000 Subject: [PATCH 20/34] fix failure level --- .../com/github/takahirom/roborazzi/sample/ComposeA11yTest.kt | 2 +- 1 file changed, 1 insertion(+), 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 9dbae80c7..60b5d48be 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 @@ -60,7 +60,7 @@ class ComposeA11yTest { preset = AccessibilityCheckPreset.LATEST, suppressions = matchesElements(withTestTag("suppress")) ), - failureLevel = CheckLevel.LogOnly, + failureLevel = CheckLevel.Warning, ) ) ) From a5737e3f5d2d53343f055e183411301849832f5c Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Mon, 18 Nov 2024 13:02:58 +0000 Subject: [PATCH 21/34] Fix --- .../roborazzi/sample/ComposeA11yTest.kt | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) 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 60b5d48be..6c082aea9 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 @@ -73,9 +73,9 @@ class ComposeA11yTest { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box( Modifier - .size(48.dp) - .background(Color.Black) - .clickable {}) + .size(48.dp) + .background(Color.Black) + .clickable {}) } } } @@ -88,11 +88,11 @@ class ComposeA11yTest { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box( Modifier - .size(48.dp) - .background(Color.Black) - .semantics { - contentDescription = "" - }) + .size(48.dp) + .background(Color.Black) + .semantics { + contentDescription = "" + }) } } } @@ -108,8 +108,8 @@ class ComposeA11yTest { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Button( onClick = {}, modifier = Modifier - .size(30.dp) - .testTag("clickable") + .size(30.dp) + .testTag("clickable") ) { Text("Something to Click") } @@ -136,12 +136,12 @@ class ComposeA11yTest { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box( Modifier - .size(48.dp) - .background(Color.Black) - .testTag("suppress") - .semantics { - contentDescription = "" - }) + .size(48.dp) + .background(Color.Black) + .testTag("suppress") + .semantics { + contentDescription = "" + }) } } } @@ -154,8 +154,8 @@ class ComposeA11yTest { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box( modifier = Modifier - .size(100.dp) - .background(Color.DarkGray) + .size(100.dp) + .background(Color.DarkGray) ) { Text("Something hard to read", color = Color.DarkGray) } @@ -169,8 +169,8 @@ class ComposeA11yTest { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box( modifier = Modifier - .size(100.dp) - .background(Color.DarkGray) + .size(100.dp) + .background(Color.DarkGray) ) { Text("Something not hard to read", color = Color.White) } @@ -188,20 +188,21 @@ class ComposeA11yTest { ) { Box( modifier = Modifier - .size(100.dp) - .background(Color.DarkGray) - .testTag("nothard") + .size(100.dp) + .background(Color.DarkGray) + .testTag("nothard") ) { Text("Something not hard to read", color = Color.White) } - Box( + // Use a single text otherwise the Box and Text both trigger and must be suppressed + Text( modifier = Modifier - .size(100.dp) - .background(Color.DarkGray) - .testTag("suppress") - ) { - Text("Something hard to read", color = Color.DarkGray) - } + .size(100.dp) + .background(Color.DarkGray) + .testTag("suppress"), + text = "Something hard to read", + color = Color.DarkGray + ) } } From ef6a2657790ad48e0c517758216e99a54bef6afc Mon Sep 17 00:00:00 2001 From: takahirom Date: Tue, 19 Nov 2024 21:54:08 +0900 Subject: [PATCH 22/34] Add checkRoboAccessibility --- .../roborazzi/ATFAccessibilityChecker.kt | 58 ++++++++++++--- .../takahirom/roborazzi/RoborazziRule.kt | 2 +- .../roborazzi/sample/ComposeA11yTest.kt | 72 ++++++++----------- .../sample/ComposeA11yWithCustomCheckTest.kt | 1 - .../roborazzi/sample/ViewA11yTest.kt | 4 +- 5 files changed, 84 insertions(+), 53 deletions(-) diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt index eeff70915..c899cd122 100644 --- a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt @@ -6,8 +6,10 @@ import android.os.Build import android.view.View import androidx.annotation.RequiresApi import androidx.compose.ui.platform.ViewRootForTest +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.CaptureRoot import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType @@ -22,6 +24,30 @@ import org.hamcrest.Matcher import org.hamcrest.Matchers import org.robolectric.shadows.ShadowBuild +fun SemanticsNodeInteraction.checkRoboAccessibility( + checker: ATFAccessibilityChecker = ATFAccessibilityChecker(), + failureLevel: CheckLevel = CheckLevel.Error, + roborazziOptions: RoborazziOptions = provideRoborazziContext().options, +) { + checker.runAccessibilityChecks( + checkNode = ATFAccessibilityChecker.CheckNode.Compose(this), + roborazziOptions = roborazziOptions, + failureLevel = failureLevel, + ) +} + +fun ViewInteraction.checkRoboAccessibility( + checker: ATFAccessibilityChecker = ATFAccessibilityChecker(), + failureLevel: CheckLevel = CheckLevel.Error, + roborazziOptions: RoborazziOptions = provideRoborazziContext().options, +) { + checker.runAccessibilityChecks( + checkNode = ATFAccessibilityChecker.CheckNode.View(this), + roborazziOptions = roborazziOptions, + failureLevel = failureLevel, + ) +} + @ExperimentalRoborazziApi data class ATFAccessibilityChecker( val checks: Set = AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset( @@ -63,9 +89,14 @@ data class ATFAccessibilityChecker( } } + internal sealed interface CheckNode { + data class View(val viewInteraction: ViewInteraction) : CheckNode + data class Compose(val semanticsNodeInteraction: SemanticsNodeInteraction) : CheckNode + } + @SuppressLint("VisibleForTests") internal fun runAccessibilityChecks( - captureRoot: CaptureRoot, + checkNode: CheckNode, roborazziOptions: RoborazziOptions, failureLevel: CheckLevel, ) { @@ -80,13 +111,15 @@ data class ATFAccessibilityChecker( ShadowBuild.setFingerprint("roborazzi") } - if (captureRoot is CaptureRoot.Compose) { - val view = (captureRoot.semanticsNodeInteraction.fetchSemanticsNode().root as ViewRootForTest).view + if (checkNode is CheckNode.Compose) { + val view = + (checkNode.semanticsNodeInteraction.fetchSemanticsNode().root as ViewRootForTest).view // Will throw based on configuration val screenshot: Bitmap? = RoboComponent.Compose( - node = captureRoot.semanticsNodeInteraction.fetchSemanticsNode(), roborazziOptions = roborazziOptions + node = checkNode.semanticsNodeInteraction.fetchSemanticsNode(), + roborazziOptions = roborazziOptions ).image val results = runAllChecks( @@ -95,8 +128,8 @@ data class ATFAccessibilityChecker( reportResults(results, failureLevel) - } else if (captureRoot is CaptureRoot.View) { - val viewInteraction = captureRoot.viewInteraction + } else if (checkNode is CheckNode.View) { + val viewInteraction = checkNode.viewInteraction // Use perform to get the view viewInteraction.perform(object : ViewAction { override fun getDescription(): String { @@ -165,7 +198,7 @@ enum class CheckLevel(private vararg val failedTypes: AccessibilityCheckResultTy } @ExperimentalRoborazziApi -data class AccessibilityChecksValidate( +data class ValidateAfterTest( val checker: ATFAccessibilityChecker = ATFAccessibilityChecker(), val failureLevel: CheckLevel = CheckLevel.Error, ) : RoborazziRule.AccessibilityChecks { @@ -173,7 +206,16 @@ data class AccessibilityChecksValidate( captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions ) { checker.runAccessibilityChecks( - captureRoot = captureRoot, + checkNode = when (captureRoot) { + is CaptureRoot.Compose -> ATFAccessibilityChecker.CheckNode.Compose( + semanticsNodeInteraction = captureRoot.semanticsNodeInteraction + ) + + CaptureRoot.None -> return + is CaptureRoot.View -> ATFAccessibilityChecker.CheckNode.View( + viewInteraction = captureRoot.viewInteraction + ) + }, roborazziOptions = roborazziOptions, failureLevel = failureLevel, ) 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 d6bc15d4d..bf1d456b5 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 @@ -66,7 +66,7 @@ class RoborazziRule private constructor( captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions, ) - // Use `roborazzi-accessibility-check`'s AccessibilityChecksValidate + // Use `roborazzi-accessibility-check`'s ValidateAfterTest data object Disabled : AccessibilityChecks { override fun runAccessibilityChecks( captureRoot: CaptureRoot, 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 6c082aea9..f54097707 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 @@ -23,12 +23,11 @@ import androidx.compose.ui.test.onRoot import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.ATFAccessibilityChecker -import com.github.takahirom.roborazzi.AccessibilityChecksValidate import com.github.takahirom.roborazzi.CheckLevel import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers -import com.github.takahirom.roborazzi.RoborazziOptions import com.github.takahirom.roborazzi.RoborazziRule import com.github.takahirom.roborazzi.RoborazziRule.Options +import com.github.takahirom.roborazzi.checkRoboAccessibility 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 @@ -73,9 +72,9 @@ class ComposeA11yTest { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box( Modifier - .size(48.dp) - .background(Color.Black) - .clickable {}) + .size(48.dp) + .background(Color.Black) + .clickable {}) } } } @@ -88,11 +87,11 @@ class ComposeA11yTest { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box( Modifier - .size(48.dp) - .background(Color.Black) - .semantics { - contentDescription = "" - }) + .size(48.dp) + .background(Color.Black) + .semantics { + contentDescription = "" + }) } } } @@ -108,8 +107,8 @@ class ComposeA11yTest { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Button( onClick = {}, modifier = Modifier - .size(30.dp) - .testTag("clickable") + .size(30.dp) + .testTag("clickable") ) { Text("Something to Click") } @@ -136,12 +135,12 @@ class ComposeA11yTest { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box( Modifier - .size(48.dp) - .background(Color.Black) - .testTag("suppress") - .semantics { - contentDescription = "" - }) + .size(48.dp) + .background(Color.Black) + .testTag("suppress") + .semantics { + contentDescription = "" + }) } } } @@ -154,8 +153,8 @@ class ComposeA11yTest { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box( modifier = Modifier - .size(100.dp) - .background(Color.DarkGray) + .size(100.dp) + .background(Color.DarkGray) ) { Text("Something hard to read", color = Color.DarkGray) } @@ -169,8 +168,8 @@ class ComposeA11yTest { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box( modifier = Modifier - .size(100.dp) - .background(Color.DarkGray) + .size(100.dp) + .background(Color.DarkGray) ) { Text("Something not hard to read", color = Color.White) } @@ -188,18 +187,18 @@ class ComposeA11yTest { ) { Box( modifier = Modifier - .size(100.dp) - .background(Color.DarkGray) - .testTag("nothard") + .size(100.dp) + .background(Color.DarkGray) + .testTag("nothard") ) { Text("Something not hard to read", color = Color.White) } // Use a single text otherwise the Box and Text both trigger and must be suppressed Text( modifier = Modifier - .size(100.dp) - .background(Color.DarkGray) - .testTag("suppress"), + .size(100.dp) + .background(Color.DarkGray) + .testTag("suppress"), text = "Something hard to read", color = Color.DarkGray ) @@ -207,22 +206,13 @@ class ComposeA11yTest { } // Now run without suppressions - val a11yChecks = AccessibilityChecksValidate( - checker = ATFAccessibilityChecker( + // Run only against nothard, shouldn't fail because of the hard to read text + composeTestRule.onNodeWithTag("nothard").checkRoboAccessibility( + ATFAccessibilityChecker( preset = AccessibilityCheckPreset.LATEST, ), - failureLevel = CheckLevel.Warning, + CheckLevel.Warning ) - - // Run only against nothard, shouldn't fail because of the hard to read text - a11yChecks - .runAccessibilityChecks( - captureRoot = RoborazziRule.CaptureRoot.Compose( - composeTestRule, - composeTestRule.onNodeWithTag("nothard"), - ), - roborazziOptions = RoborazziOptions() - ) } } 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 d1ad3f361..9d9adc457 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 @@ -15,7 +15,6 @@ import androidx.compose.ui.test.onRoot import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.ATFAccessibilityChecker -import com.github.takahirom.roborazzi.AccessibilityChecksValidate import com.github.takahirom.roborazzi.CheckLevel import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RoborazziRule 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 6d8396bfe..bb76a6c85 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 @@ -12,7 +12,7 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.ATFAccessibilityChecker -import com.github.takahirom.roborazzi.AccessibilityChecksValidate +import com.github.takahirom.roborazzi.ValidateAfterTest import com.github.takahirom.roborazzi.CheckLevel import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RoborazziRule @@ -42,7 +42,7 @@ class ViewA11yTest { val roborazziRule = RoborazziRule( captureRoot = Espresso.onView(ViewMatchers.isRoot()), options = Options( - accessibilityChecks = AccessibilityChecksValidate( + accessibilityChecks = ValidateAfterTest( checker = ATFAccessibilityChecker( preset = AccessibilityCheckPreset.LATEST, suppressions = matchesElements(withTestTag("suppress")) From 845a729a5f5784c9b12c119a53ed6c90de421b30 Mon Sep 17 00:00:00 2001 From: takahirom Date: Tue, 19 Nov 2024 22:37:31 +0900 Subject: [PATCH 23/34] Fix name --- roborazzi-accessibility-check/README.md | 2 +- .../com/github/takahirom/roborazzi/sample/ComposeA11yTest.kt | 3 ++- .../roborazzi/sample/ComposeA11yWithCustomCheckTest.kt | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/roborazzi-accessibility-check/README.md b/roborazzi-accessibility-check/README.md index f86c53a3c..ab71682da 100644 --- a/roborazzi-accessibility-check/README.md +++ b/roborazzi-accessibility-check/README.md @@ -16,7 +16,7 @@ composeRule = composeTestRule, captureRoot = composeTestRule.onRoot(), options = Options( - accessibilityChecks = AccessibilityChecksValidate( + accessibilityChecks = ValidateAfterTest( checker = ATFAccessibilityChecker( preset = AccessibilityCheckPreset.LATEST, suppressions = matchesElements(withTestTag("suppress")) 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 f54097707..b2ad16d0d 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 @@ -27,6 +27,7 @@ import com.github.takahirom.roborazzi.CheckLevel import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RoborazziRule import com.github.takahirom.roborazzi.RoborazziRule.Options +import com.github.takahirom.roborazzi.ValidateAfterTest import com.github.takahirom.roborazzi.checkRoboAccessibility import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesElements @@ -54,7 +55,7 @@ class ComposeA11yTest { composeRule = composeTestRule, captureRoot = composeTestRule.onRoot(), options = Options( - accessibilityChecks = AccessibilityChecksValidate( + accessibilityChecks = ValidateAfterTest( checker = ATFAccessibilityChecker( preset = AccessibilityCheckPreset.LATEST, suppressions = matchesElements(withTestTag("suppress")) 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 9d9adc457..478006455 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 @@ -19,6 +19,7 @@ import com.github.takahirom.roborazzi.CheckLevel import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RoborazziRule import com.github.takahirom.roborazzi.RoborazziRule.Options +import com.github.takahirom.roborazzi.ValidateAfterTest 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 @@ -59,7 +60,7 @@ class ComposeA11yWithCustomCheckTest { composeRule = composeTestRule, captureRoot = composeTestRule.onRoot(), options = Options( - accessibilityChecks = AccessibilityChecksValidate( + accessibilityChecks = ValidateAfterTest( checker = ATFAccessibilityChecker( checks = setOf(NoRedTextCheck()), suppressions = matchesElements(withTestTag("suppress")) From 32426ed8425ac26e9f563c2cae9d17d7850ee1bd Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 20 Nov 2024 10:39:17 +0900 Subject: [PATCH 24/34] Refactor naming --- ...kt => RoborazziATFAccessibilityChecker.kt} | 100 +++++++++--------- .../roborazzi/sample/ComposeA11yTest.kt | 15 ++- .../sample/ComposeA11yWithCustomCheckTest.kt | 11 +- .../roborazzi/sample/ViewA11yTest.kt | 11 +- 4 files changed, 68 insertions(+), 69 deletions(-) rename roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/{ATFAccessibilityChecker.kt => RoborazziATFAccessibilityChecker.kt} (83%) diff --git a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt similarity index 83% rename from roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt rename to roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt index c899cd122..1647d9d66 100644 --- a/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/ATFAccessibilityChecker.kt +++ b/roborazzi-accessibility-check/src/main/java/com/github/takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt @@ -25,31 +25,31 @@ import org.hamcrest.Matchers import org.robolectric.shadows.ShadowBuild fun SemanticsNodeInteraction.checkRoboAccessibility( - checker: ATFAccessibilityChecker = ATFAccessibilityChecker(), - failureLevel: CheckLevel = CheckLevel.Error, + checker: RoborazziATFAccessibilityChecker = RoborazziATFAccessibilityChecker(), + failureLevel: RoborazziATFAccessibilityChecker.CheckLevel = RoborazziATFAccessibilityChecker.CheckLevel.Error, roborazziOptions: RoborazziOptions = provideRoborazziContext().options, ) { checker.runAccessibilityChecks( - checkNode = ATFAccessibilityChecker.CheckNode.Compose(this), + checkNode = RoborazziATFAccessibilityChecker.CheckNode.Compose(this), roborazziOptions = roborazziOptions, failureLevel = failureLevel, ) } fun ViewInteraction.checkRoboAccessibility( - checker: ATFAccessibilityChecker = ATFAccessibilityChecker(), - failureLevel: CheckLevel = CheckLevel.Error, + checker: RoborazziATFAccessibilityChecker = RoborazziATFAccessibilityChecker(), + failureLevel: RoborazziATFAccessibilityChecker.CheckLevel = RoborazziATFAccessibilityChecker.CheckLevel.Error, roborazziOptions: RoborazziOptions = provideRoborazziContext().options, ) { checker.runAccessibilityChecks( - checkNode = ATFAccessibilityChecker.CheckNode.View(this), + checkNode = RoborazziATFAccessibilityChecker.CheckNode.View(this), roborazziOptions = roborazziOptions, failureLevel = failureLevel, ) } @ExperimentalRoborazziApi -data class ATFAccessibilityChecker( +data class RoborazziATFAccessibilityChecker( val checks: Set = AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset( AccessibilityCheckPreset.LATEST ), @@ -63,37 +63,26 @@ data class ATFAccessibilityChecker( suppressions = suppressions, ) + internal sealed interface CheckNode { + data class View(val viewInteraction: ViewInteraction) : CheckNode + data class Compose(val semanticsNodeInteraction: SemanticsNodeInteraction) : CheckNode + } - @RequiresApi(34) - internal fun runAllChecks( - view: View, - screenshotBitmap: Bitmap?, - checks: Set, - suppressions: Matcher, - ): List { - val parameters = Parameters().apply { - if (screenshotBitmap != null) { - putScreenCapture(BitmapImage(screenshotBitmap)) - } - setSaveViewImages(true) - } - val viewChecker = ViewChecker().apply { - setObtainCharacterLocations(true) - } + @ExperimentalRoborazziApi + enum class CheckLevel(private vararg val failedTypes: AccessibilityCheckResultType) { + Error(AccessibilityCheckResultType.ERROR), - val results = viewChecker.runChecksOnView(ImmutableSet.copyOf(checks), view, parameters) + Warning( + AccessibilityCheckResultType.ERROR, AccessibilityCheckResultType.WARNING + ), - return results.filter { - !suppressions.matches(it) - } - } + LogOnly; - internal sealed interface CheckNode { - data class View(val viewInteraction: ViewInteraction) : CheckNode - data class Compose(val semanticsNodeInteraction: SemanticsNodeInteraction) : CheckNode + fun isFailure(type: AccessibilityCheckResultType): Boolean = failedTypes.contains(type) } + @SuppressLint("VisibleForTests") internal fun runAccessibilityChecks( checkNode: CheckNode, @@ -156,6 +145,32 @@ data class ATFAccessibilityChecker( } } + @RequiresApi(34) + internal fun runAllChecks( + view: View, + screenshotBitmap: Bitmap?, + checks: Set, + suppressions: Matcher, + ): List { + val parameters = Parameters().apply { + if (screenshotBitmap != null) { + putScreenCapture(BitmapImage(screenshotBitmap)) + } + setSaveViewImages(true) + } + + val viewChecker = ViewChecker().apply { + setObtainCharacterLocations(true) + } + + val results = viewChecker.runChecksOnView(ImmutableSet.copyOf(checks), view, parameters) + + return results.filter { + !suppressions.matches(it) + } + } + + private fun reportResults( results: List, failureLevel: CheckLevel ) { @@ -185,34 +200,21 @@ data class ATFAccessibilityChecker( } @ExperimentalRoborazziApi -enum class CheckLevel(private vararg val failedTypes: AccessibilityCheckResultType) { - Error(AccessibilityCheckResultType.ERROR), - - Warning( - AccessibilityCheckResultType.ERROR, AccessibilityCheckResultType.WARNING - ), - - LogOnly; - - fun isFailure(type: AccessibilityCheckResultType): Boolean = failedTypes.contains(type) -} - -@ExperimentalRoborazziApi -data class ValidateAfterTest( - val checker: ATFAccessibilityChecker = ATFAccessibilityChecker(), - val failureLevel: CheckLevel = CheckLevel.Error, +data class AccessibilityCheckAfterTest( + val checker: RoborazziATFAccessibilityChecker = RoborazziATFAccessibilityChecker(), + val failureLevel: RoborazziATFAccessibilityChecker.CheckLevel = RoborazziATFAccessibilityChecker.CheckLevel.Error, ) : RoborazziRule.AccessibilityChecks { override fun runAccessibilityChecks( captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions ) { checker.runAccessibilityChecks( checkNode = when (captureRoot) { - is CaptureRoot.Compose -> ATFAccessibilityChecker.CheckNode.Compose( + is CaptureRoot.Compose -> RoborazziATFAccessibilityChecker.CheckNode.Compose( semanticsNodeInteraction = captureRoot.semanticsNodeInteraction ) CaptureRoot.None -> return - is CaptureRoot.View -> ATFAccessibilityChecker.CheckNode.View( + is CaptureRoot.View -> RoborazziATFAccessibilityChecker.CheckNode.View( viewInteraction = captureRoot.viewInteraction ) }, 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 b2ad16d0d..dfdf13e3a 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 @@ -22,12 +22,11 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.github.takahirom.roborazzi.ATFAccessibilityChecker -import com.github.takahirom.roborazzi.CheckLevel +import com.github.takahirom.roborazzi.AccessibilityCheckAfterTest import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers +import com.github.takahirom.roborazzi.RoborazziATFAccessibilityChecker import com.github.takahirom.roborazzi.RoborazziRule import com.github.takahirom.roborazzi.RoborazziRule.Options -import com.github.takahirom.roborazzi.ValidateAfterTest import com.github.takahirom.roborazzi.checkRoboAccessibility import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesElements @@ -55,12 +54,12 @@ class ComposeA11yTest { composeRule = composeTestRule, captureRoot = composeTestRule.onRoot(), options = Options( - accessibilityChecks = ValidateAfterTest( - checker = ATFAccessibilityChecker( + accessibilityChecks = AccessibilityCheckAfterTest( + checker = RoborazziATFAccessibilityChecker( preset = AccessibilityCheckPreset.LATEST, suppressions = matchesElements(withTestTag("suppress")) ), - failureLevel = CheckLevel.Warning, + failureLevel = RoborazziATFAccessibilityChecker.CheckLevel.Warning, ) ) ) @@ -209,10 +208,10 @@ class ComposeA11yTest { // Now run without suppressions // Run only against nothard, shouldn't fail because of the hard to read text composeTestRule.onNodeWithTag("nothard").checkRoboAccessibility( - ATFAccessibilityChecker( + RoborazziATFAccessibilityChecker( preset = AccessibilityCheckPreset.LATEST, ), - CheckLevel.Warning + RoborazziATFAccessibilityChecker.CheckLevel.Warning ) } } 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 478006455..c252ca9ca 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 @@ -14,12 +14,11 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onRoot import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.github.takahirom.roborazzi.ATFAccessibilityChecker -import com.github.takahirom.roborazzi.CheckLevel +import com.github.takahirom.roborazzi.RoborazziATFAccessibilityChecker import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RoborazziRule import com.github.takahirom.roborazzi.RoborazziRule.Options -import com.github.takahirom.roborazzi.ValidateAfterTest +import com.github.takahirom.roborazzi.AccessibilityCheckAfterTest 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 @@ -60,12 +59,12 @@ class ComposeA11yWithCustomCheckTest { composeRule = composeTestRule, captureRoot = composeTestRule.onRoot(), options = Options( - accessibilityChecks = ValidateAfterTest( - checker = ATFAccessibilityChecker( + accessibilityChecks = AccessibilityCheckAfterTest( + checker = RoborazziATFAccessibilityChecker( checks = setOf(NoRedTextCheck()), suppressions = matchesElements(withTestTag("suppress")) ), - failureLevel = CheckLevel.Warning, + failureLevel = RoborazziATFAccessibilityChecker.CheckLevel.Warning, ) ) ) 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 bb76a6c85..7cdc19830 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 @@ -11,9 +11,8 @@ import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.github.takahirom.roborazzi.ATFAccessibilityChecker -import com.github.takahirom.roborazzi.ValidateAfterTest -import com.github.takahirom.roborazzi.CheckLevel +import com.github.takahirom.roborazzi.RoborazziATFAccessibilityChecker +import com.github.takahirom.roborazzi.AccessibilityCheckAfterTest import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RoborazziRule import com.github.takahirom.roborazzi.RoborazziRule.Options @@ -42,12 +41,12 @@ class ViewA11yTest { val roborazziRule = RoborazziRule( captureRoot = Espresso.onView(ViewMatchers.isRoot()), options = Options( - accessibilityChecks = ValidateAfterTest( - checker = ATFAccessibilityChecker( + accessibilityChecks = AccessibilityCheckAfterTest( + checker = RoborazziATFAccessibilityChecker( preset = AccessibilityCheckPreset.LATEST, suppressions = matchesElements(withTestTag("suppress")) ), - failureLevel = CheckLevel.Warning, + failureLevel = RoborazziATFAccessibilityChecker.CheckLevel.Warning, ) ) ) From 77fa9dc1dc9e68a797cc942405a9134dd52a27df Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 20 Nov 2024 11:18:50 +0900 Subject: [PATCH 25/34] Refactor to make AccessibilityChecker configurable for each checkRoboAccessibility. --- .../roborazzi/AccessibilityChecker.kt | 3 ++ .../takahirom/roborazzi/RoborazziContext.kt | 16 ++++++++ .../RoborazziATFAccessibilityChecker.kt | 41 +++++++++++-------- .../takahirom/roborazzi/RoborazziRule.kt | 13 +++--- .../roborazzi/sample/ComposeA11yTest.kt | 10 ++--- .../sample/ComposeA11yWithCustomCheckTest.kt | 14 +++---- .../roborazzi/sample/ViewA11yTest.kt | 12 +++--- 7 files changed, 68 insertions(+), 41 deletions(-) create mode 100644 include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/AccessibilityChecker.kt diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/AccessibilityChecker.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/AccessibilityChecker.kt new file mode 100644 index 000000000..f8712f784 --- /dev/null +++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/AccessibilityChecker.kt @@ -0,0 +1,3 @@ +package com.github.takahirom.roborazzi + +interface AccessibilityChecker \ No newline at end of file diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziContext.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziContext.kt index 15ce447e7..9aadf778f 100644 --- a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziContext.kt +++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziContext.kt @@ -63,6 +63,22 @@ class RoborazziContextImpl { ruleOverrideImageExtension = null } + private var ruleOverrideAccessibilityChecker: AccessibilityChecker? = null + + @InternalRoborazziApi + fun setRuleOverrideAccessibilityChecker(checker: AccessibilityChecker?) { + ruleOverrideAccessibilityChecker = checker + } + + @InternalRoborazziApi + fun clearRuleOverrideAccessibilityChecker() { + ruleOverrideAccessibilityChecker = null + } + + @InternalRoborazziApi + val accessibilityChecker: AccessibilityChecker? + get() = ruleOverrideAccessibilityChecker + @InternalRoborazziApi val imageExtension: String get() = ruleOverrideImageExtension ?: roborazziSystemPropertyImageExtension() 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 1647d9d66..b79d7b463 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 @@ -25,7 +25,7 @@ import org.hamcrest.Matchers import org.robolectric.shadows.ShadowBuild fun SemanticsNodeInteraction.checkRoboAccessibility( - checker: RoborazziATFAccessibilityChecker = RoborazziATFAccessibilityChecker(), + checker: RoborazziATFAccessibilityChecker = provideATFAccessibilityCheckerOrCreateDefault(), failureLevel: RoborazziATFAccessibilityChecker.CheckLevel = RoborazziATFAccessibilityChecker.CheckLevel.Error, roborazziOptions: RoborazziOptions = provideRoborazziContext().options, ) { @@ -37,7 +37,7 @@ fun SemanticsNodeInteraction.checkRoboAccessibility( } fun ViewInteraction.checkRoboAccessibility( - checker: RoborazziATFAccessibilityChecker = RoborazziATFAccessibilityChecker(), + checker: RoborazziATFAccessibilityChecker = provideATFAccessibilityCheckerOrCreateDefault(), failureLevel: RoborazziATFAccessibilityChecker.CheckLevel = RoborazziATFAccessibilityChecker.CheckLevel.Error, roborazziOptions: RoborazziOptions = provideRoborazziContext().options, ) { @@ -48,13 +48,18 @@ fun ViewInteraction.checkRoboAccessibility( ) } +private fun provideATFAccessibilityCheckerOrCreateDefault() = + ((provideRoborazziContext().accessibilityChecker as? RoborazziATFAccessibilityChecker) + ?: RoborazziATFAccessibilityChecker()) + + @ExperimentalRoborazziApi data class RoborazziATFAccessibilityChecker( val checks: Set = AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset( AccessibilityCheckPreset.LATEST ), val suppressions: Matcher = Matchers.not(Matchers.anything()), -) { +) : AccessibilityChecker { constructor( preset: AccessibilityCheckPreset, suppressions: Matcher = Matchers.not(Matchers.anything()), @@ -201,25 +206,25 @@ data class RoborazziATFAccessibilityChecker( @ExperimentalRoborazziApi data class AccessibilityCheckAfterTest( - val checker: RoborazziATFAccessibilityChecker = RoborazziATFAccessibilityChecker(), val failureLevel: RoborazziATFAccessibilityChecker.CheckLevel = RoborazziATFAccessibilityChecker.CheckLevel.Error, -) : RoborazziRule.AccessibilityChecks { +) : RoborazziRule.AccessibilityCheckStrategy { override fun runAccessibilityChecks( captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions ) { - checker.runAccessibilityChecks( - checkNode = when (captureRoot) { - is CaptureRoot.Compose -> RoborazziATFAccessibilityChecker.CheckNode.Compose( - semanticsNodeInteraction = captureRoot.semanticsNodeInteraction - ) + provideATFAccessibilityCheckerOrCreateDefault() + .runAccessibilityChecks( + checkNode = when (captureRoot) { + is CaptureRoot.Compose -> RoborazziATFAccessibilityChecker.CheckNode.Compose( + semanticsNodeInteraction = captureRoot.semanticsNodeInteraction + ) - CaptureRoot.None -> return - is CaptureRoot.View -> RoborazziATFAccessibilityChecker.CheckNode.View( - viewInteraction = captureRoot.viewInteraction - ) - }, - roborazziOptions = roborazziOptions, - failureLevel = failureLevel, - ) + CaptureRoot.None -> return + is CaptureRoot.View -> RoborazziATFAccessibilityChecker.CheckNode.View( + viewInteraction = captureRoot.viewInteraction + ) + }, + roborazziOptions = roborazziOptions, + failureLevel = failureLevel, + ) } } 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 bf1d456b5..674d1afcb 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 @@ -56,18 +56,19 @@ class RoborazziRule private constructor( val outputFileProvider: FileProvider = provideRoborazziContext().fileProvider ?: defaultFileProvider, val roborazziOptions: RoborazziOptions = provideRoborazziContext().options, - val accessibilityChecks: AccessibilityChecks = AccessibilityChecks.Disabled, + val accessibilityChecker: AccessibilityChecker? = null, + val accessibilityCheckStrategy: AccessibilityCheckStrategy = AccessibilityCheckStrategy.None, ) @ExperimentalRoborazziApi - interface AccessibilityChecks { + interface AccessibilityCheckStrategy { @InternalRoborazziApi fun runAccessibilityChecks( captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions, ) - // Use `roborazzi-accessibility-check`'s ValidateAfterTest - data object Disabled : AccessibilityChecks { + // Use `roborazzi-accessibility-check`'s AccessibilityCheckAfterTest + data object None : AccessibilityCheckStrategy { override fun runAccessibilityChecks( captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions @@ -161,12 +162,14 @@ class RoborazziRule private constructor( provideRoborazziContext().setRuleOverrideRoborazziOptions(options.roborazziOptions) provideRoborazziContext().setRuleOverrideFileProvider(options.outputFileProvider) provideRoborazziContext().setRuleOverrideDescription(description) + provideRoborazziContext().setRuleOverrideAccessibilityChecker(options.accessibilityChecker) runTest(base, description, captureRoot) } finally { provideRoborazziContext().clearRuleOverrideOutputDirectory() provideRoborazziContext().clearRuleOverrideRoborazziOptions() provideRoborazziContext().clearRuleOverrideFileProvider() provideRoborazziContext().clearRuleOverrideDescription() + provideRoborazziContext().clearRuleOverrideAccessibilityChecker() } } } @@ -179,7 +182,7 @@ class RoborazziRule private constructor( ) { val evaluate: () -> Unit = { try { - val accessibilityChecks = options.accessibilityChecks + val accessibilityChecks = options.accessibilityCheckStrategy // TODO enable a11y before showing content base.evaluate() 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 dfdf13e3a..4cbcf888b 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 @@ -54,11 +54,11 @@ class ComposeA11yTest { composeRule = composeTestRule, captureRoot = composeTestRule.onRoot(), options = Options( - accessibilityChecks = AccessibilityCheckAfterTest( - checker = RoborazziATFAccessibilityChecker( - preset = AccessibilityCheckPreset.LATEST, - suppressions = matchesElements(withTestTag("suppress")) - ), + accessibilityChecker = RoborazziATFAccessibilityChecker( + preset = AccessibilityCheckPreset.LATEST, + suppressions = matchesElements(withTestTag("suppress")) + ), + accessibilityCheckStrategy = AccessibilityCheckAfterTest( failureLevel = RoborazziATFAccessibilityChecker.CheckLevel.Warning, ) ) 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 c252ca9ca..b421e540b 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 @@ -14,11 +14,11 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onRoot import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.github.takahirom.roborazzi.RoborazziATFAccessibilityChecker +import com.github.takahirom.roborazzi.AccessibilityCheckAfterTest import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers +import com.github.takahirom.roborazzi.RoborazziATFAccessibilityChecker import com.github.takahirom.roborazzi.RoborazziRule import com.github.takahirom.roborazzi.RoborazziRule.Options -import com.github.takahirom.roborazzi.AccessibilityCheckAfterTest 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 @@ -59,11 +59,11 @@ class ComposeA11yWithCustomCheckTest { composeRule = composeTestRule, captureRoot = composeTestRule.onRoot(), options = Options( - accessibilityChecks = AccessibilityCheckAfterTest( - checker = RoborazziATFAccessibilityChecker( - checks = setOf(NoRedTextCheck()), - suppressions = matchesElements(withTestTag("suppress")) - ), + accessibilityChecker = RoborazziATFAccessibilityChecker( + checks = setOf(NoRedTextCheck()), + suppressions = matchesElements(withTestTag("suppress")) + ), + accessibilityCheckStrategy = AccessibilityCheckAfterTest( failureLevel = RoborazziATFAccessibilityChecker.CheckLevel.Warning, ) ) 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 7cdc19830..1c70f17e4 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 @@ -11,9 +11,9 @@ import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.github.takahirom.roborazzi.RoborazziATFAccessibilityChecker import com.github.takahirom.roborazzi.AccessibilityCheckAfterTest import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers +import com.github.takahirom.roborazzi.RoborazziATFAccessibilityChecker import com.github.takahirom.roborazzi.RoborazziRule import com.github.takahirom.roborazzi.RoborazziRule.Options import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset @@ -41,11 +41,11 @@ class ViewA11yTest { val roborazziRule = RoborazziRule( captureRoot = Espresso.onView(ViewMatchers.isRoot()), options = Options( - accessibilityChecks = AccessibilityCheckAfterTest( - checker = RoborazziATFAccessibilityChecker( - preset = AccessibilityCheckPreset.LATEST, - suppressions = matchesElements(withTestTag("suppress")) - ), + accessibilityChecker = RoborazziATFAccessibilityChecker( + preset = AccessibilityCheckPreset.LATEST, + suppressions = matchesElements(withTestTag("suppress")) + ), + accessibilityCheckStrategy = AccessibilityCheckAfterTest( failureLevel = RoborazziATFAccessibilityChecker.CheckLevel.Warning, ) ) From 7885e2b085bc4099e3aac0224a4616da29aee5ae Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 20 Nov 2024 11:21:05 +0900 Subject: [PATCH 26/34] Fix README --- roborazzi-accessibility-check/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/roborazzi-accessibility-check/README.md b/roborazzi-accessibility-check/README.md index ab71682da..184b4cf0d 100644 --- a/roborazzi-accessibility-check/README.md +++ b/roborazzi-accessibility-check/README.md @@ -16,11 +16,11 @@ composeRule = composeTestRule, captureRoot = composeTestRule.onRoot(), options = Options( - accessibilityChecks = ValidateAfterTest( - checker = ATFAccessibilityChecker( - preset = AccessibilityCheckPreset.LATEST, - suppressions = matchesElements(withTestTag("suppress")) - ), + accessibilityChecker = RoborazziATFAccessibilityChecker( + checks = setOf(NoRedTextCheck()), + suppressions = matchesElements(withTestTag("suppress")) + ), + accessibilityChecks = AccessibilityCheckAfterTest( failureLevel = CheckLevel.Warning, ) ) From 2032a4f820d35976288ea0f2f2791b5f1d82a693 Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 20 Nov 2024 11:44:41 +0900 Subject: [PATCH 27/34] Add RoborazziAccessibilityOptions to maintain options --- .../roborazzi/AccessibilityChecker.kt | 3 -- .../RoborazziAccessibilityOptions.kt | 5 +++ .../takahirom/roborazzi/RoborazziContext.kt | 14 ++++---- roborazzi-accessibility-check/README.md | 12 +++---- .../RoborazziATFAccessibilityChecker.kt | 34 +++++++++++-------- .../takahirom/roborazzi/RoborazziRule.kt | 6 ++-- .../roborazzi/sample/ComposeA11yTest.kt | 24 +++++++------ .../sample/ComposeA11yWithCustomCheckTest.kt | 14 ++++---- .../roborazzi/sample/ViewA11yTest.kt | 14 ++++---- 9 files changed, 68 insertions(+), 58 deletions(-) delete mode 100644 include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/AccessibilityChecker.kt create mode 100644 include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziAccessibilityOptions.kt diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/AccessibilityChecker.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/AccessibilityChecker.kt deleted file mode 100644 index f8712f784..000000000 --- a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/AccessibilityChecker.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.github.takahirom.roborazzi - -interface AccessibilityChecker \ No newline at end of file diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziAccessibilityOptions.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziAccessibilityOptions.kt new file mode 100644 index 000000000..2d9a47aa6 --- /dev/null +++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziAccessibilityOptions.kt @@ -0,0 +1,5 @@ +package com.github.takahirom.roborazzi + +interface RoborazziAccessibilityOptions { + object None: RoborazziAccessibilityOptions +} diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziContext.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziContext.kt index 9aadf778f..a04840ff3 100644 --- a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziContext.kt +++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziContext.kt @@ -63,21 +63,21 @@ class RoborazziContextImpl { ruleOverrideImageExtension = null } - private var ruleOverrideAccessibilityChecker: AccessibilityChecker? = null + private var ruleOverrideRoborazziAccessibilityOptions: RoborazziAccessibilityOptions? = null @InternalRoborazziApi - fun setRuleOverrideAccessibilityChecker(checker: AccessibilityChecker?) { - ruleOverrideAccessibilityChecker = checker + fun setRuleOverrideAccessibilityOptions(checker: RoborazziAccessibilityOptions?) { + ruleOverrideRoborazziAccessibilityOptions = checker } @InternalRoborazziApi - fun clearRuleOverrideAccessibilityChecker() { - ruleOverrideAccessibilityChecker = null + fun clearRuleOverrideAccessibilityOptions() { + ruleOverrideRoborazziAccessibilityOptions = null } @InternalRoborazziApi - val accessibilityChecker: AccessibilityChecker? - get() = ruleOverrideAccessibilityChecker + val roborazziAccessibilityOptions: RoborazziAccessibilityOptions + get() = ruleOverrideRoborazziAccessibilityOptions ?: RoborazziAccessibilityOptions.None @InternalRoborazziApi val imageExtension: String diff --git a/roborazzi-accessibility-check/README.md b/roborazzi-accessibility-check/README.md index 184b4cf0d..565212e22 100644 --- a/roborazzi-accessibility-check/README.md +++ b/roborazzi-accessibility-check/README.md @@ -16,13 +16,13 @@ composeRule = composeTestRule, captureRoot = composeTestRule.onRoot(), options = Options( - accessibilityChecker = RoborazziATFAccessibilityChecker( - checks = setOf(NoRedTextCheck()), - suppressions = matchesElements(withTestTag("suppress")) + roborazziAccessibilityOptions = RoborazziATFAccessibilityCheckOptions( + checker = RoborazziATFAccessibilityChecker( + checks = setOf(NoRedTextCheck()), + suppressions = matchesElements(withTestTag("suppress")) + ), + failureLevel = RoborazziATFAccessibilityChecker.CheckLevel.Warning ), - accessibilityChecks = AccessibilityCheckAfterTest( - failureLevel = CheckLevel.Warning, - ) ) ) ``` 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 b79d7b463..7cec4bed2 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 @@ -25,33 +25,35 @@ import org.hamcrest.Matchers import org.robolectric.shadows.ShadowBuild fun SemanticsNodeInteraction.checkRoboAccessibility( - checker: RoborazziATFAccessibilityChecker = provideATFAccessibilityCheckerOrCreateDefault(), - failureLevel: RoborazziATFAccessibilityChecker.CheckLevel = RoborazziATFAccessibilityChecker.CheckLevel.Error, + roborazziATFAccessibilityCheckOptions: RoborazziATFAccessibilityCheckOptions = provideATFAccessibilityOptionsOrCreateDefault(), roborazziOptions: RoborazziOptions = provideRoborazziContext().options, ) { - checker.runAccessibilityChecks( + roborazziATFAccessibilityCheckOptions.checker.runAccessibilityChecks( checkNode = RoborazziATFAccessibilityChecker.CheckNode.Compose(this), roborazziOptions = roborazziOptions, - failureLevel = failureLevel, + failureLevel = roborazziATFAccessibilityCheckOptions.failureLevel, ) } fun ViewInteraction.checkRoboAccessibility( - checker: RoborazziATFAccessibilityChecker = provideATFAccessibilityCheckerOrCreateDefault(), - failureLevel: RoborazziATFAccessibilityChecker.CheckLevel = RoborazziATFAccessibilityChecker.CheckLevel.Error, + roborazziATFAccessibilityCheckOptions: RoborazziATFAccessibilityCheckOptions = RoborazziATFAccessibilityCheckOptions(), roborazziOptions: RoborazziOptions = provideRoborazziContext().options, ) { - checker.runAccessibilityChecks( + roborazziATFAccessibilityCheckOptions.checker.runAccessibilityChecks( checkNode = RoborazziATFAccessibilityChecker.CheckNode.View(this), roborazziOptions = roborazziOptions, - failureLevel = failureLevel, + failureLevel = roborazziATFAccessibilityCheckOptions.failureLevel, ) } -private fun provideATFAccessibilityCheckerOrCreateDefault() = - ((provideRoborazziContext().accessibilityChecker as? RoborazziATFAccessibilityChecker) - ?: RoborazziATFAccessibilityChecker()) +private fun provideATFAccessibilityOptionsOrCreateDefault(): RoborazziATFAccessibilityCheckOptions = + ((provideRoborazziContext().roborazziAccessibilityOptions as? RoborazziATFAccessibilityCheckOptions) + ?: RoborazziATFAccessibilityCheckOptions()) +data class RoborazziATFAccessibilityCheckOptions( + val checker: RoborazziATFAccessibilityChecker = RoborazziATFAccessibilityChecker(), + val failureLevel: RoborazziATFAccessibilityChecker.CheckLevel = RoborazziATFAccessibilityChecker.CheckLevel.Error, +) : RoborazziAccessibilityOptions @ExperimentalRoborazziApi data class RoborazziATFAccessibilityChecker( @@ -59,7 +61,7 @@ data class RoborazziATFAccessibilityChecker( AccessibilityCheckPreset.LATEST ), val suppressions: Matcher = Matchers.not(Matchers.anything()), -) : AccessibilityChecker { +) : RoborazziAccessibilityOptions { constructor( preset: AccessibilityCheckPreset, suppressions: Matcher = Matchers.not(Matchers.anything()), @@ -206,12 +208,14 @@ data class RoborazziATFAccessibilityChecker( @ExperimentalRoborazziApi data class AccessibilityCheckAfterTest( - val failureLevel: RoborazziATFAccessibilityChecker.CheckLevel = RoborazziATFAccessibilityChecker.CheckLevel.Error, + val accessibilityOptionsFactory: () -> RoborazziATFAccessibilityCheckOptions = { provideATFAccessibilityOptionsOrCreateDefault() }, ) : RoborazziRule.AccessibilityCheckStrategy { override fun runAccessibilityChecks( captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions ) { - provideATFAccessibilityCheckerOrCreateDefault() + val accessibilityOptions = accessibilityOptionsFactory() + accessibilityOptions + .checker .runAccessibilityChecks( checkNode = when (captureRoot) { is CaptureRoot.Compose -> RoborazziATFAccessibilityChecker.CheckNode.Compose( @@ -224,7 +228,7 @@ data class AccessibilityCheckAfterTest( ) }, roborazziOptions = roborazziOptions, - failureLevel = failureLevel, + failureLevel = accessibilityOptions.failureLevel, ) } } 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 674d1afcb..802f8ae5f 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 @@ -56,7 +56,7 @@ class RoborazziRule private constructor( val outputFileProvider: FileProvider = provideRoborazziContext().fileProvider ?: defaultFileProvider, val roborazziOptions: RoborazziOptions = provideRoborazziContext().options, - val accessibilityChecker: AccessibilityChecker? = null, + val roborazziAccessibilityOptions: RoborazziAccessibilityOptions = provideRoborazziContext().roborazziAccessibilityOptions, val accessibilityCheckStrategy: AccessibilityCheckStrategy = AccessibilityCheckStrategy.None, ) @@ -162,14 +162,14 @@ class RoborazziRule private constructor( provideRoborazziContext().setRuleOverrideRoborazziOptions(options.roborazziOptions) provideRoborazziContext().setRuleOverrideFileProvider(options.outputFileProvider) provideRoborazziContext().setRuleOverrideDescription(description) - provideRoborazziContext().setRuleOverrideAccessibilityChecker(options.accessibilityChecker) + provideRoborazziContext().setRuleOverrideAccessibilityOptions(options.roborazziAccessibilityOptions) runTest(base, description, captureRoot) } finally { provideRoborazziContext().clearRuleOverrideOutputDirectory() provideRoborazziContext().clearRuleOverrideRoborazziOptions() provideRoborazziContext().clearRuleOverrideFileProvider() provideRoborazziContext().clearRuleOverrideDescription() - provideRoborazziContext().clearRuleOverrideAccessibilityChecker() + provideRoborazziContext().clearRuleOverrideAccessibilityOptions() } } } 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 4cbcf888b..8f33c61ab 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 @@ -24,6 +24,7 @@ import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.AccessibilityCheckAfterTest 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 @@ -54,13 +55,14 @@ class ComposeA11yTest { composeRule = composeTestRule, captureRoot = composeTestRule.onRoot(), options = Options( - accessibilityChecker = RoborazziATFAccessibilityChecker( - preset = AccessibilityCheckPreset.LATEST, - suppressions = matchesElements(withTestTag("suppress")) - ), - accessibilityCheckStrategy = AccessibilityCheckAfterTest( + roborazziAccessibilityOptions = RoborazziATFAccessibilityCheckOptions( + RoborazziATFAccessibilityChecker( + preset = AccessibilityCheckPreset.LATEST, + suppressions = matchesElements(withTestTag("suppress")), + ), failureLevel = RoborazziATFAccessibilityChecker.CheckLevel.Warning, - ) + ), + accessibilityCheckStrategy = AccessibilityCheckAfterTest() ) ) @@ -208,10 +210,12 @@ class ComposeA11yTest { // Now run without suppressions // Run only against nothard, shouldn't fail because of the hard to read text composeTestRule.onNodeWithTag("nothard").checkRoboAccessibility( - RoborazziATFAccessibilityChecker( - preset = AccessibilityCheckPreset.LATEST, - ), - RoborazziATFAccessibilityChecker.CheckLevel.Warning + RoborazziATFAccessibilityCheckOptions( + RoborazziATFAccessibilityChecker( + preset = AccessibilityCheckPreset.LATEST, + ), + RoborazziATFAccessibilityChecker.CheckLevel.Warning + ) ) } } 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 b421e540b..22c88dfea 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 @@ -14,8 +14,8 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onRoot import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.github.takahirom.roborazzi.AccessibilityCheckAfterTest 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 @@ -59,13 +59,13 @@ class ComposeA11yWithCustomCheckTest { composeRule = composeTestRule, captureRoot = composeTestRule.onRoot(), options = Options( - accessibilityChecker = RoborazziATFAccessibilityChecker( - checks = setOf(NoRedTextCheck()), - suppressions = matchesElements(withTestTag("suppress")) + roborazziAccessibilityOptions = RoborazziATFAccessibilityCheckOptions( + checker = RoborazziATFAccessibilityChecker( + checks = setOf(NoRedTextCheck()), + suppressions = matchesElements(withTestTag("suppress")) + ), + failureLevel = RoborazziATFAccessibilityChecker.CheckLevel.Warning ), - accessibilityCheckStrategy = AccessibilityCheckAfterTest( - failureLevel = RoborazziATFAccessibilityChecker.CheckLevel.Warning, - ) ) ) 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 1c70f17e4..d3b526e54 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 @@ -11,8 +11,8 @@ import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.github.takahirom.roborazzi.AccessibilityCheckAfterTest 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 @@ -41,12 +41,12 @@ class ViewA11yTest { val roborazziRule = RoborazziRule( captureRoot = Espresso.onView(ViewMatchers.isRoot()), options = Options( - accessibilityChecker = RoborazziATFAccessibilityChecker( - preset = AccessibilityCheckPreset.LATEST, - suppressions = matchesElements(withTestTag("suppress")) - ), - accessibilityCheckStrategy = AccessibilityCheckAfterTest( - failureLevel = RoborazziATFAccessibilityChecker.CheckLevel.Warning, + roborazziAccessibilityOptions = RoborazziATFAccessibilityCheckOptions( + checker = RoborazziATFAccessibilityChecker( + preset = AccessibilityCheckPreset.LATEST, + suppressions = matchesElements(withTestTag("suppress")) + ), + failureLevel = RoborazziATFAccessibilityChecker.CheckLevel.Warning ) ) ) From 9457576489aa57dd4dae2f31a3f2900f1273f9bc Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 20 Nov 2024 12:02:20 +0900 Subject: [PATCH 28/34] Fix test --- .../takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt | 2 +- .../com/github/takahirom/roborazzi/sample/ComposeA11yTest.kt | 4 ++-- .../roborazzi/sample/ComposeA11yWithCustomCheckTest.kt | 2 ++ .../com/github/takahirom/roborazzi/sample/ViewA11yTest.kt | 4 +++- 4 files changed, 8 insertions(+), 4 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 7cec4bed2..e703d41d4 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 @@ -207,7 +207,7 @@ data class RoborazziATFAccessibilityChecker( } @ExperimentalRoborazziApi -data class AccessibilityCheckAfterTest( +data class AccessibilityCheckAfterTestStrategy( val accessibilityOptionsFactory: () -> RoborazziATFAccessibilityCheckOptions = { provideATFAccessibilityOptionsOrCreateDefault() }, ) : RoborazziRule.AccessibilityCheckStrategy { override fun runAccessibilityChecks( 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 8f33c61ab..621912c57 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 @@ -22,7 +22,7 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.github.takahirom.roborazzi.AccessibilityCheckAfterTest +import com.github.takahirom.roborazzi.AccessibilityCheckAfterTestStrategy import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RoborazziATFAccessibilityCheckOptions import com.github.takahirom.roborazzi.RoborazziATFAccessibilityChecker @@ -62,7 +62,7 @@ class ComposeA11yTest { ), failureLevel = RoborazziATFAccessibilityChecker.CheckLevel.Warning, ), - accessibilityCheckStrategy = AccessibilityCheckAfterTest() + accessibilityCheckStrategy = AccessibilityCheckAfterTestStrategy() ) ) 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 22c88dfea..b35f32859 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 @@ -14,6 +14,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onRoot import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.github.takahirom.roborazzi.AccessibilityCheckAfterTestStrategy import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RoborazziATFAccessibilityCheckOptions import com.github.takahirom.roborazzi.RoborazziATFAccessibilityChecker @@ -66,6 +67,7 @@ class ComposeA11yWithCustomCheckTest { ), failureLevel = RoborazziATFAccessibilityChecker.CheckLevel.Warning ), + accessibilityCheckStrategy = AccessibilityCheckAfterTestStrategy(), ) ) 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 d3b526e54..4dc49c38a 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 @@ -11,6 +11,7 @@ import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.github.takahirom.roborazzi.AccessibilityCheckAfterTestStrategy import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RoborazziATFAccessibilityCheckOptions import com.github.takahirom.roborazzi.RoborazziATFAccessibilityChecker @@ -47,7 +48,8 @@ class ViewA11yTest { suppressions = matchesElements(withTestTag("suppress")) ), failureLevel = RoborazziATFAccessibilityChecker.CheckLevel.Warning - ) + ), + accessibilityCheckStrategy = AccessibilityCheckAfterTestStrategy() ) ) From be07298bad84ce474f2bd5b1560010dd64c6f881 Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 20 Nov 2024 12:05:09 +0900 Subject: [PATCH 29/34] Fix comment --- .../main/java/com/github/takahirom/roborazzi/RoborazziRule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 802f8ae5f..bbc6d4972 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 @@ -67,7 +67,7 @@ class RoborazziRule private constructor( captureRoot: CaptureRoot, roborazziOptions: RoborazziOptions, ) - // Use `roborazzi-accessibility-check`'s AccessibilityCheckAfterTest + // Use `roborazzi-accessibility-check`'s [com.github.takahirom.roborazzi.AccessibilityCheckAfterTestStrategy] data object None : AccessibilityCheckStrategy { override fun runAccessibilityChecks( captureRoot: CaptureRoot, From b500d2ca95038595cd9f584c2e6d5ebbc07599b9 Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 20 Nov 2024 12:07:12 +0900 Subject: [PATCH 30/34] Add README document --- roborazzi-accessibility-check/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/roborazzi-accessibility-check/README.md b/roborazzi-accessibility-check/README.md index 565212e22..03c4c9b87 100644 --- a/roborazzi-accessibility-check/README.md +++ b/roborazzi-accessibility-check/README.md @@ -23,10 +23,26 @@ ), failureLevel = RoborazziATFAccessibilityChecker.CheckLevel.Warning ), + // If you want to check a11y after test, use AccessibilityCheckAfterTestStrategy + accessibilityCheckStrategy = AccessibilityCheckAfterTestStrategy(), ) ) ``` +### Add a11y checks + +```kotlin +composeTestRule.onNodeWithTag("nothard").checkRoboAccessibility( + // You can also use the roborazziAccessibilityOptions from RoborazziRule + RoborazziATFAccessibilityCheckOptions( + RoborazziATFAccessibilityChecker( + preset = AccessibilityCheckPreset.LATEST, + ), + RoborazziATFAccessibilityChecker.CheckLevel.Warning + ) +) +``` + ### Checking Log output Particularly with `failureLevel = CheckLevel.LogOnly` the output log of each test will including a11y checks. From c05f6936a7df49ac5e9a6c4c0da354fc15737c20 Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 20 Nov 2024 12:11:40 +0900 Subject: [PATCH 31/34] Fix README document --- roborazzi-accessibility-check/README.md | 10 +++++----- .../takahirom/roborazzi/sample/ComposeA11yTest.kt | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/roborazzi-accessibility-check/README.md b/roborazzi-accessibility-check/README.md index 03c4c9b87..d2325e2d7 100644 --- a/roborazzi-accessibility-check/README.md +++ b/roborazzi-accessibility-check/README.md @@ -23,7 +23,7 @@ ), failureLevel = RoborazziATFAccessibilityChecker.CheckLevel.Warning ), - // If you want to check a11y after test, use AccessibilityCheckAfterTestStrategy + // If you want to automatically check accessibility after a test, use AccessibilityCheckAfterTestStrategy. accessibilityCheckStrategy = AccessibilityCheckAfterTestStrategy(), ) ) @@ -33,12 +33,12 @@ ```kotlin composeTestRule.onNodeWithTag("nothard").checkRoboAccessibility( - // You can also use the roborazziAccessibilityOptions from RoborazziRule - RoborazziATFAccessibilityCheckOptions( - RoborazziATFAccessibilityChecker( + // If you don't specify options, the options in RoborazziRule will be used. + roborazziATFAccessibilityCheckOptions = RoborazziATFAccessibilityCheckOptions( + checker = RoborazziATFAccessibilityChecker( preset = AccessibilityCheckPreset.LATEST, ), - RoborazziATFAccessibilityChecker.CheckLevel.Warning + failureLevel = RoborazziATFAccessibilityChecker.CheckLevel.Warning ) ) ``` 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 621912c57..19be6f919 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 @@ -210,11 +210,11 @@ class ComposeA11yTest { // Now run without suppressions // Run only against nothard, shouldn't fail because of the hard to read text composeTestRule.onNodeWithTag("nothard").checkRoboAccessibility( - RoborazziATFAccessibilityCheckOptions( - RoborazziATFAccessibilityChecker( + roborazziATFAccessibilityCheckOptions = RoborazziATFAccessibilityCheckOptions( + checker = RoborazziATFAccessibilityChecker( preset = AccessibilityCheckPreset.LATEST, ), - RoborazziATFAccessibilityChecker.CheckLevel.Warning + failureLevel = RoborazziATFAccessibilityChecker.CheckLevel.Warning ) ) } From 61c521f9350a9b9729f75498f517962ad792629f Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 20 Nov 2024 21:21:01 +0900 Subject: [PATCH 32/34] Add @ExperimentalRoborazziApi --- .../takahirom/roborazzi/RoborazziAccessibilityOptions.kt | 2 ++ .../takahirom/roborazzi/RoborazziATFAccessibilityChecker.kt | 3 +++ 2 files changed, 5 insertions(+) diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziAccessibilityOptions.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziAccessibilityOptions.kt index 2d9a47aa6..3344496b7 100644 --- a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziAccessibilityOptions.kt +++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziAccessibilityOptions.kt @@ -1,5 +1,7 @@ package com.github.takahirom.roborazzi +@ExperimentalRoborazziApi interface RoborazziAccessibilityOptions { + // See `roborazzi-accessibility-check`'s [com.github.takahirom.roborazzi.RoborazziATFAccessibilityCheckOptions] object None: RoborazziAccessibilityOptions } 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 e703d41d4..1a5512f7b 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 @@ -24,6 +24,7 @@ import org.hamcrest.Matcher import org.hamcrest.Matchers import org.robolectric.shadows.ShadowBuild +@ExperimentalRoborazziApi fun SemanticsNodeInteraction.checkRoboAccessibility( roborazziATFAccessibilityCheckOptions: RoborazziATFAccessibilityCheckOptions = provideATFAccessibilityOptionsOrCreateDefault(), roborazziOptions: RoborazziOptions = provideRoborazziContext().options, @@ -35,6 +36,7 @@ fun SemanticsNodeInteraction.checkRoboAccessibility( ) } +@ExperimentalRoborazziApi fun ViewInteraction.checkRoboAccessibility( roborazziATFAccessibilityCheckOptions: RoborazziATFAccessibilityCheckOptions = RoborazziATFAccessibilityCheckOptions(), roborazziOptions: RoborazziOptions = provideRoborazziContext().options, @@ -50,6 +52,7 @@ private fun provideATFAccessibilityOptionsOrCreateDefault(): RoborazziATFAccessi ((provideRoborazziContext().roborazziAccessibilityOptions as? RoborazziATFAccessibilityCheckOptions) ?: RoborazziATFAccessibilityCheckOptions()) +@ExperimentalRoborazziApi data class RoborazziATFAccessibilityCheckOptions( val checker: RoborazziATFAccessibilityChecker = RoborazziATFAccessibilityChecker(), val failureLevel: RoborazziATFAccessibilityChecker.CheckLevel = RoborazziATFAccessibilityChecker.CheckLevel.Error, From 1079f3843a89167690e8caa6c07d1cb41ac5249d Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 20 Nov 2024 21:43:06 +0900 Subject: [PATCH 33/34] Add a test and fix bug --- .../RoborazziATFAccessibilityChecker.kt | 2 +- .../roborazzi/sample/ViewA11yTest.kt | 26 ++++++++++++++++++- 2 files changed, 26 insertions(+), 2 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 1a5512f7b..21114ce26 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 @@ -38,7 +38,7 @@ fun SemanticsNodeInteraction.checkRoboAccessibility( @ExperimentalRoborazziApi fun ViewInteraction.checkRoboAccessibility( - roborazziATFAccessibilityCheckOptions: RoborazziATFAccessibilityCheckOptions = RoborazziATFAccessibilityCheckOptions(), + roborazziATFAccessibilityCheckOptions: RoborazziATFAccessibilityCheckOptions = provideATFAccessibilityOptionsOrCreateDefault(), roborazziOptions: RoborazziOptions = provideRoborazziContext().options, ) { roborazziATFAccessibilityCheckOptions.checker.runAccessibilityChecks( 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 4dc49c38a..7a8bad220 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,6 +17,7 @@ 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.checkRoboAccessibility 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 @@ -115,6 +116,29 @@ class ViewA11yTest { } } + + @Test + fun checkRoboAccessibilityCheck() { + thrown.expectMessage("TextContrastCheck") + activityScenarioRule.scenario.onActivity { activity -> + val frameLayout = FrameLayout(activity) + activity.setContentView( + frameLayout.apply { + setBackgroundColor(Color.BLACK) + addView( + TextView(activity).apply { + text = "Something hard to read" + setTextColor(Color.DKGRAY) + } + ) + } + ) + onView(ViewMatchers.isRoot()) + .checkRoboAccessibility() + frameLayout.removeAllViews() + } + } + @Test fun supressionsTakeEffect() { activityScenarioRule.scenario.onActivity { activity -> @@ -162,7 +186,7 @@ class ViewA11yTest { setBackgroundColor(Color.DKGRAY) addView( TextView(activity).apply { - text = "Something hard to read" + text = "Something not hard to read" setTextColor(Color.WHITE) } ) From 927424bd0ab2c3adccf3abfb827e0c1061d236ac Mon Sep 17 00:00:00 2001 From: takahirom Date: Wed, 20 Nov 2024 22:49:21 +0900 Subject: [PATCH 34/34] Add Accessibility Check to README --- README.md | 5 +++++ docs/topics/how_to_use.md | 5 +++++ roborazzi-accessibility-check/README.md | 5 ++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 24cb19c59..88b797365 100644 --- a/README.md +++ b/README.md @@ -927,6 +927,11 @@ If you are having trouble debugging your test, try Dump mode as follows. ![image](https://user-images.githubusercontent.com/1386930/226364158-a07a0fb0-d8e7-46b7-a495-8dd217faaadb.png) +### Accessibility Check + +Roborazzi Accessibility Checks is a library that integrates accessibility checks into Roborazzi. +Please refer to [Accessibility Check](https://github.com/takahirom/roborazzi/blob/main/roborazzi-accessibility-check/README.md) + ### Roborazzi options Please check out [RoborazziOptions](https://github.com/takahirom/roborazzi/blob/main/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt) for available Roborazzi options. diff --git a/docs/topics/how_to_use.md b/docs/topics/how_to_use.md index e95be494a..066a2625c 100644 --- a/docs/topics/how_to_use.md +++ b/docs/topics/how_to_use.md @@ -604,6 +604,11 @@ If you are having trouble debugging your test, try Dump mode as follows. ![image](https://user-images.githubusercontent.com/1386930/226364158-a07a0fb0-d8e7-46b7-a495-8dd217faaadb.png) +### Accessibility Check + +Roborazzi Accessibility Checks is a library that integrates accessibility checks into Roborazzi. +Please refer to [Accessibility Check](https://github.com/takahirom/roborazzi/blob/main/roborazzi-accessibility-check/README.md) + ### Roborazzi options Please check out [RoborazziOptions](https://github.com/takahirom/roborazzi/blob/main/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt) for available Roborazzi options. diff --git a/roborazzi-accessibility-check/README.md b/roborazzi-accessibility-check/README.md index d2325e2d7..ca76bbeaf 100644 --- a/roborazzi-accessibility-check/README.md +++ b/roborazzi-accessibility-check/README.md @@ -1,5 +1,8 @@ # Roborazzi Acessibility Checks +Roborazzi Accessibility Checks is a library that integrates accessibility checks into Roborazzi. +It uses [ Accessibility Test Framework](https://github.com/google/Accessibility-Test-Framework-for-Android) to check accessibility. + ## How to use ### Add dependencies @@ -29,7 +32,7 @@ ) ``` -### Add a11y checks +### Add accessibility checks ```kotlin composeTestRule.onNodeWithTag("nothard").checkRoboAccessibility(