Merge pull request #1708 from yschimke/introduce_screenshot_a11y_tests

Introduce screenshot accessibility tests
pull/1732/head
Don Turner 2 weeks ago committed by GitHub
commit 6bf2d04d59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -26,6 +26,7 @@ android {
dependencies { dependencies {
api(libs.bundles.androidx.compose.ui.test) api(libs.bundles.androidx.compose.ui.test)
api(libs.roborazzi) api(libs.roborazzi)
api(libs.roborazzi.accessibility.check)
implementation(libs.androidx.compose.ui.test) implementation(libs.androidx.compose.ui.test)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.robolectric) implementation(libs.robolectric)

@ -14,8 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
@file:OptIn(ExperimentalRoborazziApi::class)
package com.google.samples.apps.nowinandroid.core.testing.util package com.google.samples.apps.nowinandroid.core.testing.util
import android.graphics.Bitmap.CompressFormat.PNG
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -30,12 +33,25 @@ import androidx.compose.ui.test.DeviceConfigurationOverride
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.onRoot
import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.rules.ActivityScenarioRule
import com.github.takahirom.roborazzi.ExperimentalRoborazziApi
import com.github.takahirom.roborazzi.RoborazziATFAccessibilityCheckOptions
import com.github.takahirom.roborazzi.RoborazziATFAccessibilityChecker
import com.github.takahirom.roborazzi.RoborazziATFAccessibilityChecker.CheckLevel
import com.github.takahirom.roborazzi.RoborazziOptions import com.github.takahirom.roborazzi.RoborazziOptions
import com.github.takahirom.roborazzi.RoborazziOptions.CompareOptions import com.github.takahirom.roborazzi.RoborazziOptions.CompareOptions
import com.github.takahirom.roborazzi.RoborazziOptions.RecordOptions import com.github.takahirom.roborazzi.RoborazziOptions.RecordOptions
import com.github.takahirom.roborazzi.captureRoboImage import com.github.takahirom.roborazzi.captureRoboImage
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.AccessibilityViewCheckResult
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.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import org.hamcrest.Matcher
import org.hamcrest.Matchers
import org.robolectric.RuntimeEnvironment import org.robolectric.RuntimeEnvironment
import java.io.File
import java.io.FileOutputStream
val DefaultRoborazziOptions = val DefaultRoborazziOptions =
RoborazziOptions( RoborazziOptions(
@ -52,10 +68,17 @@ enum class DefaultTestDevices(val description: String, val spec: String) {
} }
fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>.captureMultiDevice( fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>.captureMultiDevice(
screenshotName: String, screenshotName: String,
accessibilitySuppressions: Matcher<in AccessibilityViewCheckResult> = Matchers.not(Matchers.anything()),
body: @Composable () -> Unit, body: @Composable () -> Unit,
) { ) {
DefaultTestDevices.entries.forEach { DefaultTestDevices.entries.forEach {
this.captureForDevice(it.description, it.spec, screenshotName, body = body) this.captureForDevice(
deviceName = it.description,
deviceSpec = it.spec,
screenshotName = screenshotName,
body = body,
accessibilitySuppressions = accessibilitySuppressions,
)
} }
} }
@ -64,6 +87,7 @@ fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>.c
deviceSpec: String, deviceSpec: String,
screenshotName: String, screenshotName: String,
roborazziOptions: RoborazziOptions = DefaultRoborazziOptions, roborazziOptions: RoborazziOptions = DefaultRoborazziOptions,
accessibilitySuppressions: Matcher<in AccessibilityViewCheckResult> = Matchers.not(Matchers.anything()),
darkMode: Boolean = false, darkMode: Boolean = false,
body: @Composable () -> Unit, body: @Composable () -> Unit,
) { ) {
@ -83,11 +107,46 @@ fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>.c
} }
} }
} }
// Run Accessibility checks first so logging is included
val accessibilityException = try {
this.onRoot().checkRoboAccessibility(
roborazziATFAccessibilityCheckOptions = RoborazziATFAccessibilityCheckOptions(
failureLevel = CheckLevel.Error,
checker = RoborazziATFAccessibilityChecker(
preset = AccessibilityCheckPreset.LATEST,
suppressions = accessibilitySuppressions,
),
),
)
null
} catch (e: AccessibilityViewCheckException) {
e
}
this.onRoot() this.onRoot()
.captureRoboImage( .captureRoboImage(
"src/test/screenshots/${screenshotName}_$deviceName.png", "src/test/screenshots/${screenshotName}_$deviceName.png",
roborazziOptions = roborazziOptions, roborazziOptions = roborazziOptions,
) )
// Rethrow the Accessibility exception once screenshots have passed
if (accessibilityException != null) {
accessibilityException.results.forEachIndexed { index, check ->
val viewImage = check.viewImage
if (viewImage is BitmapImage) {
val file = File("build/outputs/roborazzi/${screenshotName}_${deviceName}_$index.png")
println("Writing check.viewImage to $file")
FileOutputStream(
file,
).use {
viewImage.bitmap.compress(PNG, 100, it)
}
}
}
throw accessibilityException
}
} }
/** /**

@ -19,6 +19,10 @@ package com.google.samples.apps.nowinandroid.feature.foryou
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesElements
import com.google.android.apps.common.testing.accessibility.framework.checks.TextContrastCheck
import com.google.android.apps.common.testing.accessibility.framework.matcher.ElementMatchers.withText
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.testing.util.DefaultTestDevices import com.google.samples.apps.nowinandroid.core.testing.util.DefaultTestDevices
@ -31,6 +35,7 @@ import com.google.samples.apps.nowinandroid.feature.foryou.OnboardingUiState.Loa
import com.google.samples.apps.nowinandroid.feature.foryou.OnboardingUiState.NotShown import com.google.samples.apps.nowinandroid.feature.foryou.OnboardingUiState.NotShown
import com.google.samples.apps.nowinandroid.feature.foryou.OnboardingUiState.Shown import com.google.samples.apps.nowinandroid.feature.foryou.OnboardingUiState.Shown
import dagger.hilt.android.testing.HiltTestApplication import dagger.hilt.android.testing.HiltTestApplication
import org.hamcrest.Matchers
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -108,7 +113,20 @@ class ForYouScreenScreenshotTests {
@Test @Test
fun forYouScreenTopicSelection() { fun forYouScreenTopicSelection() {
composeTestRule.captureMultiDevice("ForYouScreenTopicSelection") { composeTestRule.captureMultiDevice(
"ForYouScreenTopicSelection",
accessibilitySuppressions = Matchers.allOf(
AccessibilityCheckResultUtils.matchesCheck(TextContrastCheck::class.java),
Matchers.anyOf(
// Disabled Button
matchesElements(withText("Done")),
// TODO investigate, seems a false positive
matchesElements(withText("What are you interested in?")),
matchesElements(withText("UI")),
),
),
) {
ForYouScreenTopicSelection() ForYouScreenTopicSelection()
} }
} }

@ -141,6 +141,7 @@ retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.r
retrofit-kotlin-serialization = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" } retrofit-kotlin-serialization = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" }
robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" }
roborazzi = { group = "io.github.takahirom.roborazzi", name = "roborazzi", version.ref = "roborazzi" } roborazzi = { group = "io.github.takahirom.roborazzi", name = "roborazzi", version.ref = "roborazzi" }
roborazzi-accessibility-check = { group = "io.github.takahirom.roborazzi", name = "roborazzi-accessibility-check", version.ref = "roborazzi" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }

Loading…
Cancel
Save