Skip to content

Commit

Permalink
Separate roborazzi-accessibility-check module
Browse files Browse the repository at this point in the history
  • Loading branch information
takahirom committed Nov 17, 2024
1 parent addf887 commit 2be749b
Show file tree
Hide file tree
Showing 14 changed files with 216 additions and 83 deletions.
1 change: 1 addition & 0 deletions roborazzi-accessibility-check/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
44 changes: 44 additions & 0 deletions roborazzi-accessibility-check/build.gradle
Original file line number Diff line number Diff line change
@@ -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
}
Empty file.
21 changes: 21 additions & 0 deletions roborazzi-accessibility-check/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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)
}
}
4 changes: 4 additions & 0 deletions roborazzi-accessibility-check/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>

</manifest>
Original file line number Diff line number Diff line change
@@ -1,66 +1,19 @@
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
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


@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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AccessibilityHierarchyCheck>,
val suppressions: Matcher<in AccessibilityViewCheckResult>,
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,
)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
1 change: 0 additions & 1 deletion roborazzi-junit-rule/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -65,21 +60,20 @@ class RoborazziRule private constructor(
)

@ExperimentalRoborazziApi
data class ATFAccessibilityChecker(
val checks: Set<AccessibilityHierarchyCheck>,
val suppressions: Matcher<in AccessibilityViewCheckResult>,
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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions sample-android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -67,7 +67,7 @@ class ComposeA11yTest {
applyDeviceCrop = true
),
),
accessibilityChecks = AccessibilityChecks.Validate(
accessibilityChecks = AccessibilityChecksValidate(
checker = atfAccessibilityChecker
)
)
Expand Down
Loading

0 comments on commit 2be749b

Please sign in to comment.